001package com.box.sdk;
002
003import java.io.ByteArrayOutputStream;
004import java.io.IOException;
005import java.io.InputStream;
006import java.io.OutputStream;
007import java.net.MalformedURLException;
008import java.net.URL;
009import java.util.ArrayList;
010import java.util.Collection;
011import java.util.Date;
012import java.util.EnumSet;
013import java.util.List;
014
015import com.eclipsesource.json.JsonArray;
016import com.eclipsesource.json.JsonObject;
017import com.eclipsesource.json.JsonValue;
018
019
020/**
021 * Represents an individual file on Box. This class can be used to download a file's contents, upload new versions, and
022 * perform other common file operations (move, copy, delete, etc.).
023 *
024 * <p>Unless otherwise noted, the methods in this class can throw an unchecked {@link BoxAPIException} (unchecked
025 * meaning that the compiler won't force you to handle it) if an error occurs. If you wish to implement custom error
026 * handling for errors related to the Box REST API, you should capture this exception explicitly.</p>
027 */
028public class BoxFile extends BoxItem {
029
030    /**
031     * An array of all possible file fields that can be requested when calling {@link #getInfo()}.
032     */
033    public static final String[] ALL_FIELDS = {"type", "id", "sequence_id", "etag", "sha1", "name",
034        "description", "size", "path_collection", "created_at", "modified_at", "trashed_at", "purged_at",
035        "content_created_at", "content_modified_at", "created_by", "modified_by", "owned_by", "shared_link", "parent",
036        "item_status", "version_number", "comment_count", "permissions", "tags", "lock", "extension", "is_package",
037        "file_version"};
038
039    /**
040     * Used to specify what filetype to request for a file thumbnail.
041     */
042    public enum ThumbnailFileType {
043        /**
044         * PNG image format.
045         */
046        PNG,
047
048        /**
049         * JPG image format.
050         */
051        JPG
052    }
053
054    private static final URLTemplate FILE_URL_TEMPLATE = new URLTemplate("files/%s");
055    private static final URLTemplate CONTENT_URL_TEMPLATE = new URLTemplate("files/%s/content");
056    private static final URLTemplate VERSIONS_URL_TEMPLATE = new URLTemplate("files/%s/versions");
057    private static final URLTemplate COPY_URL_TEMPLATE = new URLTemplate("files/%s/copy");
058    private static final URLTemplate ADD_COMMENT_URL_TEMPLATE = new URLTemplate("comments");
059    private static final URLTemplate GET_COMMENTS_URL_TEMPLATE = new URLTemplate("files/%s/comments");
060    private static final URLTemplate METADATA_URL_TEMPLATE = new URLTemplate("files/%s/metadata/%s/%s");
061    private static final URLTemplate ADD_TASK_URL_TEMPLATE = new URLTemplate("tasks");
062    private static final URLTemplate GET_TASKS_URL_TEMPLATE = new URLTemplate("files/%s/tasks");
063    private static final URLTemplate GET_THUMBNAIL_PNG_TEMPLATE = new URLTemplate("files/%s/thumbnail.png");
064    private static final URLTemplate GET_THUMBNAIL_JPG_TEMPLATE = new URLTemplate("files/%s/thumbnail.jpg");
065    private static final String DEFAULT_METADATA_TYPE = "properties";
066    private static final String GLOBAL_METADATA_SCOPE = "global";
067    private static final String ENTERPRISE_METADATA_SCOPE = "enterprise";
068    private static final int BUFFER_SIZE = 8192;
069
070
071    /**
072     * Constructs a BoxFile for a file with a given ID.
073     * @param  api the API connection to be used by the file.
074     * @param  id  the ID of the file.
075     */
076    public BoxFile(BoxAPIConnection api, String id) {
077        super(api, id);
078    }
079
080    @Override
081    public BoxSharedLink createSharedLink(BoxSharedLink.Access access, Date unshareDate,
082        BoxSharedLink.Permissions permissions) {
083
084        BoxSharedLink sharedLink = new BoxSharedLink(access, unshareDate, permissions);
085        Info info = new Info();
086        info.setSharedLink(sharedLink);
087
088        this.updateInfo(info);
089        return info.getSharedLink();
090    }
091
092    /**
093     * Adds a comment to this file. The message can contain @mentions by using the string @[userid:username] anywhere
094     * within the message, where userid and username are the ID and username of the person being mentioned.
095     * @see    <a href="https://developers.box.com/docs/#comments-add-a-comment-to-an-item">the tagged_message field
096     *         for including @mentions.</a>
097     * @param  message the comment's message.
098     * @return information about the newly added comment.
099     */
100    public BoxComment.Info addComment(String message) {
101        JsonObject itemJSON = new JsonObject();
102        itemJSON.add("type", "file");
103        itemJSON.add("id", this.getID());
104
105        JsonObject requestJSON = new JsonObject();
106        requestJSON.add("item", itemJSON);
107        if (BoxComment.messageContainsMention(message)) {
108            requestJSON.add("tagged_message", message);
109        } else {
110            requestJSON.add("message", message);
111        }
112
113        URL url = ADD_COMMENT_URL_TEMPLATE.build(this.getAPI().getBaseURL());
114        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "POST");
115        request.setBody(requestJSON.toString());
116        BoxJSONResponse response = (BoxJSONResponse) request.send();
117        JsonObject responseJSON = JsonObject.readFrom(response.getJSON());
118
119        BoxComment addedComment = new BoxComment(this.getAPI(), responseJSON.get("id").asString());
120        return addedComment.new Info(responseJSON);
121    }
122
123    /**
124     * Adds a new task to this file. The task can have an optional message to include, and a due date.
125     * @param action the action the task assignee will be prompted to do.
126     * @param message an optional message to include with the task.
127     * @param dueAt the day at which this task is due.
128     * @return information about the newly added task.
129     */
130    public BoxTask.Info addTask(BoxTask.Action action, String message, Date dueAt) {
131        JsonObject itemJSON = new JsonObject();
132        itemJSON.add("type", "file");
133        itemJSON.add("id", this.getID());
134
135        JsonObject requestJSON = new JsonObject();
136        requestJSON.add("item", itemJSON);
137        requestJSON.add("action", action.toJSONString());
138
139        if (message != null && !message.isEmpty()) {
140            requestJSON.add("message", message);
141        }
142
143        if (dueAt != null) {
144            requestJSON.add("due_at", dueAt.toString());
145        }
146
147        URL url = ADD_TASK_URL_TEMPLATE.build(this.getAPI().getBaseURL());
148        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "POST");
149        request.setBody(requestJSON.toString());
150        BoxJSONResponse response = (BoxJSONResponse) request.send();
151        JsonObject responseJSON = JsonObject.readFrom(response.getJSON());
152
153        BoxTask addedTask = new BoxTask(this.getAPI(), responseJSON.get("id").asString());
154        return addedTask.new Info(responseJSON);
155    }
156
157    /**
158     * Gets an expiring URL for downloading a file directly from Box. This can be user,
159     * for example, for sending as a redirect to a browser to cause the browser
160     * to download the file directly from Box.
161     * @return the temporary download URL
162     */
163    public URL getDownloadURL() {
164        URL url = CONTENT_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
165        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
166        request.setFollowRedirects(false);
167
168        BoxRedirectResponse response = (BoxRedirectResponse) request.send();
169
170        return response.getRedirectURL();
171    }
172
173    /**
174     * Downloads the contents of this file to a given OutputStream.
175     * @param output the stream to where the file will be written.
176     */
177    public void download(OutputStream output) {
178        this.download(output, null);
179    }
180
181    /**
182     * Downloads the contents of this file to a given OutputStream while reporting the progress to a ProgressListener.
183     * @param output   the stream to where the file will be written.
184     * @param listener a listener for monitoring the download's progress.
185     */
186    public void download(OutputStream output, ProgressListener listener) {
187        URL url = CONTENT_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
188        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
189        BoxAPIResponse response = request.send();
190        InputStream input = response.getBody(listener);
191
192        byte[] buffer = new byte[BUFFER_SIZE];
193        try {
194            int n = input.read(buffer);
195            while (n != -1) {
196                output.write(buffer, 0, n);
197                n = input.read(buffer);
198            }
199        } catch (IOException e) {
200            throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e);
201        } finally {
202            response.disconnect();
203        }
204    }
205
206    /**
207     * Downloads a part of this file's contents, starting at specified byte offset.
208     * @param output the stream to where the file will be written.
209     * @param offset the byte offset at which to start the download.
210     */
211    public void downloadRange(OutputStream output, long offset) {
212        this.downloadRange(output, offset, -1);
213    }
214
215    /**
216     * Downloads a part of this file's contents, starting at rangeStart and stopping at rangeEnd.
217     * @param output     the stream to where the file will be written.
218     * @param rangeStart the byte offset at which to start the download.
219     * @param rangeEnd   the byte offset at which to stop the download.
220     */
221    public void downloadRange(OutputStream output, long rangeStart, long rangeEnd) {
222        this.downloadRange(output, rangeStart, rangeEnd, null);
223    }
224
225    /**
226     * Downloads a part of this file's contents, starting at rangeStart and stopping at rangeEnd, while reporting the
227     * progress to a ProgressListener.
228     * @param output     the stream to where the file will be written.
229     * @param rangeStart the byte offset at which to start the download.
230     * @param rangeEnd   the byte offset at which to stop the download.
231     * @param listener   a listener for monitoring the download's progress.
232     */
233    public void downloadRange(OutputStream output, long rangeStart, long rangeEnd, ProgressListener listener) {
234        URL url = CONTENT_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
235        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
236        if (rangeEnd > 0) {
237            request.addHeader("Range", String.format("bytes=%s-%s", Long.toString(rangeStart),
238                Long.toString(rangeEnd)));
239        } else {
240            request.addHeader("Range", String.format("bytes=%s-", Long.toString(rangeStart)));
241        }
242
243        BoxAPIResponse response = request.send();
244        InputStream input = response.getBody(listener);
245
246        byte[] buffer = new byte[BUFFER_SIZE];
247        try {
248            int n = input.read(buffer);
249            while (n != -1) {
250                output.write(buffer, 0, n);
251                n = input.read(buffer);
252            }
253        } catch (IOException e) {
254            throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e);
255        } finally {
256            response.disconnect();
257        }
258    }
259
260    @Override
261    public BoxFile.Info copy(BoxFolder destination) {
262        return this.copy(destination, null);
263    }
264
265    @Override
266    public BoxFile.Info copy(BoxFolder destination, String newName) {
267        URL url = COPY_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
268
269        JsonObject parent = new JsonObject();
270        parent.add("id", destination.getID());
271
272        JsonObject copyInfo = new JsonObject();
273        copyInfo.add("parent", parent);
274        if (newName != null) {
275            copyInfo.add("name", newName);
276        }
277
278        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "POST");
279        request.setBody(copyInfo.toString());
280        BoxJSONResponse response = (BoxJSONResponse) request.send();
281        JsonObject responseJSON = JsonObject.readFrom(response.getJSON());
282        BoxFile copiedFile = new BoxFile(this.getAPI(), responseJSON.get("id").asString());
283        return copiedFile.new Info(responseJSON);
284    }
285
286    /**
287     * Deletes this file by moving it to the trash.
288     */
289    public void delete() {
290        URL url = FILE_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
291        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "DELETE");
292        BoxAPIResponse response = request.send();
293        response.disconnect();
294    }
295
296    @Override
297    public BoxItem.Info move(BoxFolder destination) {
298        return this.move(destination, null);
299    }
300
301    @Override
302    public BoxItem.Info move(BoxFolder destination, String newName) {
303        URL url = FILE_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
304        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "PUT");
305
306        JsonObject parent = new JsonObject();
307        parent.add("id", destination.getID());
308
309        JsonObject updateInfo = new JsonObject();
310        updateInfo.add("parent", parent);
311        if (newName != null) {
312            updateInfo.add("name", newName);
313        }
314
315        request.setBody(updateInfo.toString());
316        BoxJSONResponse response = (BoxJSONResponse) request.send();
317        JsonObject responseJSON = JsonObject.readFrom(response.getJSON());
318        BoxFile movedFile = new BoxFile(this.getAPI(), responseJSON.get("id").asString());
319        return movedFile.new Info(responseJSON);
320    }
321
322    /**
323     * Renames this file.
324     * @param newName the new name of the file.
325     */
326    public void rename(String newName) {
327        URL url = FILE_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
328        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "PUT");
329
330        JsonObject updateInfo = new JsonObject();
331        updateInfo.add("name", newName);
332
333        request.setBody(updateInfo.toString());
334        BoxAPIResponse response = request.send();
335        response.disconnect();
336    }
337
338    @Override
339    public BoxFile.Info getInfo() {
340        URL url = FILE_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
341        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
342        BoxJSONResponse response = (BoxJSONResponse) request.send();
343        return new Info(response.getJSON());
344    }
345
346    @Override
347    public BoxFile.Info getInfo(String... fields) {
348        String queryString = new QueryStringBuilder().appendParam("fields", fields).toString();
349        URL url = FILE_URL_TEMPLATE.buildWithQuery(this.getAPI().getBaseURL(), queryString, this.getID());
350
351        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
352        BoxJSONResponse response = (BoxJSONResponse) request.send();
353        return new Info(response.getJSON());
354    }
355
356    /**
357     * Updates the information about this file with any info fields that have been modified locally.
358     *
359     * <p>The only fields that will be updated are the ones that have been modified locally. For example, the following
360     * code won't update any information (or even send a network request) since none of the info's fields were
361     * changed:</p>
362     *
363     * <pre>BoxFile file = new File(api, id);
364     *BoxFile.Info info = file.getInfo();
365     *file.updateInfo(info);</pre>
366     *
367     * @param info the updated info.
368     */
369    public void updateInfo(BoxFile.Info info) {
370        URL url = FILE_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
371        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "PUT");
372        request.setBody(info.getPendingChanges());
373        BoxJSONResponse response = (BoxJSONResponse) request.send();
374        JsonObject jsonObject = JsonObject.readFrom(response.getJSON());
375        info.update(jsonObject);
376    }
377
378    /**
379     * Gets any previous versions of this file. Note that only users with premium accounts will be able to retrieve
380     * previous versions of their files.
381     * @return a list of previous file versions.
382     */
383    public Collection<BoxFileVersion> getVersions() {
384        URL url = VERSIONS_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
385        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
386        BoxJSONResponse response = (BoxJSONResponse) request.send();
387
388        JsonObject jsonObject = JsonObject.readFrom(response.getJSON());
389        JsonArray entries = jsonObject.get("entries").asArray();
390        Collection<BoxFileVersion> versions = new ArrayList<BoxFileVersion>();
391        for (JsonValue entry : entries) {
392            versions.add(new BoxFileVersion(this.getAPI(), entry.asObject(), this.getID()));
393        }
394
395        return versions;
396    }
397
398    /**
399     * Checks if the file can be successfully uploaded by using the preflight check.
400     * @param  name        the name to give the uploaded file or null to use existing name.
401     * @param  fileSize    the size of the file used for account capacity calculations.
402     * @param  parentID    the ID of the parent folder that the new version is being uploaded to.
403     */
404    public void canUploadVersion(String name, long fileSize, String parentID) {
405        URL url = CONTENT_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
406        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "OPTIONS");
407
408        JsonObject parent = new JsonObject();
409        parent.add("id", parentID);
410
411        JsonObject preflightInfo = new JsonObject();
412        preflightInfo.add("parent", parent);
413        if (name != null) {
414            preflightInfo.add("name", name);
415        }
416
417        preflightInfo.add("size", fileSize);
418
419        request.setBody(preflightInfo.toString());
420        BoxAPIResponse response = request.send();
421        response.disconnect();
422    }
423
424    /**
425     * Uploads a new version of this file, replacing the current version. Note that only users with premium accounts
426     * will be able to view and recover previous versions of the file.
427     * @param fileContent a stream containing the new file contents.
428     */
429    public void uploadVersion(InputStream fileContent) {
430        this.uploadVersion(fileContent, null);
431    }
432
433    /**
434     * Uploads a new version of this file, replacing the current version. Note that only users with premium accounts
435     * will be able to view and recover previous versions of the file.
436     * @param fileContent a stream containing the new file contents.
437     * @param modified    the date that the new version was modified.
438     */
439    public void uploadVersion(InputStream fileContent, Date modified) {
440        this.uploadVersion(fileContent, modified, 0, null);
441    }
442
443    /**
444     * Uploads a new version of this file, replacing the current version, while reporting the progress to a
445     * ProgressListener. Note that only users with premium accounts will be able to view and recover previous versions
446     * of the file.
447     * @param fileContent a stream containing the new file contents.
448     * @param modified    the date that the new version was modified.
449     * @param fileSize    the size of the file used for determining the progress of the upload.
450     * @param listener    a listener for monitoring the upload's progress.
451     */
452    public void uploadVersion(InputStream fileContent, Date modified, long fileSize, ProgressListener listener) {
453        URL uploadURL = CONTENT_URL_TEMPLATE.build(this.getAPI().getBaseUploadURL(), this.getID());
454        BoxMultipartRequest request = new BoxMultipartRequest(getAPI(), uploadURL);
455        if (fileSize > 0) {
456            request.setFile(fileContent, "", fileSize);
457        } else {
458            request.setFile(fileContent, "");
459        }
460
461        if (modified != null) {
462            request.putField("content_modified_at", modified);
463        }
464
465        BoxAPIResponse response;
466        if (listener == null) {
467            response = request.send();
468        } else {
469            response = request.send(listener);
470        }
471        response.disconnect();
472    }
473
474    /**
475     * Gets an expiring URL for creating an embedded preview session. The URL will expire after 60 seconds and the
476     * preview session will expire after 60 minutes.
477     * @return the expiring preview link
478     */
479    public URL getPreviewLink() {
480        BoxFile.Info info = this.getInfo("expiring_embed_link");
481
482        return info.getPreviewLink();
483    }
484
485
486    /**
487     * Retrieves a thumbnail, or smaller image representation, of this file. Sizes of 32x32, 64x64, 128x128,
488     * and 256x256 can be returned in the .png format and sizes of 32x32, 94x94, 160x160, and 320x320 can be returned
489     * in the .jpg format.
490     * @param fileType      either PNG of JPG
491     * @param minWidth      minimum width
492     * @param minHeight     minimum height
493     * @param maxWidth      maximum width
494     * @param maxHeight     maximum height
495     * @return the byte array of the thumbnail image
496     */
497    public byte[] getThumbnail(ThumbnailFileType fileType, int minWidth, int minHeight, int maxWidth, int maxHeight) {
498        QueryStringBuilder builder = new QueryStringBuilder();
499        builder.appendParam("min_width", minWidth);
500        builder.appendParam("min_height", minHeight);
501        builder.appendParam("max_width", maxWidth);
502        builder.appendParam("max_height", maxHeight);
503
504        URLTemplate template;
505        if (fileType == ThumbnailFileType.PNG) {
506            template = GET_THUMBNAIL_PNG_TEMPLATE;
507        } else if (fileType == ThumbnailFileType.JPG) {
508            template = GET_THUMBNAIL_JPG_TEMPLATE;
509        } else {
510            throw new BoxAPIException("Unsupported thumbnail file type");
511        }
512        URL url = template.buildWithQuery(this.getAPI().getBaseURL(), builder.toString(), this.getID());
513
514        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
515        BoxAPIResponse response = request.send();
516
517        ByteArrayOutputStream thumbOut = new ByteArrayOutputStream();
518        InputStream body = response.getBody();
519        byte[] buffer = new byte[BUFFER_SIZE];
520        try {
521            int n = body.read(buffer);
522            while (n != -1) {
523                thumbOut.write(buffer, 0, n);
524                n = body.read(buffer);
525            }
526        } catch (IOException e) {
527            throw new BoxAPIException("Error reading thumbnail bytes from response body", e);
528        } finally {
529            response.disconnect();
530        }
531
532        return thumbOut.toByteArray();
533    }
534
535    /**
536     * Gets a list of any comments on this file.
537     * @return a list of comments on this file.
538     */
539    public List<BoxComment.Info> getComments() {
540        URL url = GET_COMMENTS_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
541        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
542        BoxJSONResponse response = (BoxJSONResponse) request.send();
543        JsonObject responseJSON = JsonObject.readFrom(response.getJSON());
544
545        int totalCount = responseJSON.get("total_count").asInt();
546        List<BoxComment.Info> comments = new ArrayList<BoxComment.Info>(totalCount);
547        JsonArray entries = responseJSON.get("entries").asArray();
548        for (JsonValue value : entries) {
549            JsonObject commentJSON = value.asObject();
550            BoxComment comment = new BoxComment(this.getAPI(), commentJSON.get("id").asString());
551            BoxComment.Info info = comment.new Info(commentJSON);
552            comments.add(info);
553        }
554
555        return comments;
556    }
557
558    /**
559     * Gets a list of any tasks on this file.
560     * @return a list of tasks on this file.
561     */
562    public List<BoxTask.Info> getTasks() {
563        URL url = GET_TASKS_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
564        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
565        BoxJSONResponse response = (BoxJSONResponse) request.send();
566        JsonObject responseJSON = JsonObject.readFrom(response.getJSON());
567
568        int totalCount = responseJSON.get("total_count").asInt();
569        List<BoxTask.Info> tasks = new ArrayList<BoxTask.Info>(totalCount);
570        JsonArray entries = responseJSON.get("entries").asArray();
571        for (JsonValue value : entries) {
572            JsonObject taskJSON = value.asObject();
573            BoxTask task = new BoxTask(this.getAPI(), taskJSON.get("id").asString());
574            BoxTask.Info info = task.new Info(taskJSON);
575            tasks.add(info);
576        }
577
578        return tasks;
579    }
580
581    /**
582     * Creates metadata on this file in the global properties template.
583     * @param metadata The new metadata values.
584     * @return the metadata returned from the server.
585     */
586    public Metadata createMetadata(Metadata metadata) {
587        return this.createMetadata(DEFAULT_METADATA_TYPE, metadata);
588    }
589
590    /**
591     * Creates metadata on this file in the specified template type.
592     * @param typeName the metadata template type name.
593     * @param metadata the new metadata values.
594     * @return the metadata returned from the server.
595     */
596    public Metadata createMetadata(String typeName, Metadata metadata) {
597        String scope = this.scopeBasedOnType(typeName);
598        URL url = METADATA_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID(), scope, typeName);
599        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "POST");
600        request.addHeader("Content-Type", "application/json");
601        request.setBody(metadata.toString());
602        BoxJSONResponse response = (BoxJSONResponse) request.send();
603        return new Metadata(JsonObject.readFrom(response.getJSON()));
604    }
605
606    /**
607     * Gets the file properties metadata.
608     * @return the metadata returned from the server.
609     */
610    public Metadata getMetadata() {
611        return this.getMetadata(DEFAULT_METADATA_TYPE);
612    }
613
614    /**
615     * Gets the file metadata of specified template type.
616     * @param typeName the metadata template type name.
617     * @return the metadata returned from the server.
618     */
619    public Metadata getMetadata(String typeName) {
620        String scope = this.scopeBasedOnType(typeName);
621        URL url = METADATA_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID(), scope, typeName);
622        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
623        BoxJSONResponse response = (BoxJSONResponse) request.send();
624        return new Metadata(JsonObject.readFrom(response.getJSON()));
625    }
626
627    /**
628     * Updates the file metadata.
629     * @param metadata the new metadata values.
630     * @return the metadata returned from the server.
631     */
632    public Metadata updateMetadata(Metadata metadata) {
633        String scope;
634        if (metadata.getScope().equals(GLOBAL_METADATA_SCOPE)) {
635            scope = GLOBAL_METADATA_SCOPE;
636        } else {
637            scope = ENTERPRISE_METADATA_SCOPE;
638        }
639
640        URL url = METADATA_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID(),
641                                                scope, metadata.getTemplateName());
642        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "PUT");
643        request.addHeader("Content-Type", "application/json-patch+json");
644        request.setBody(metadata.getPatch());
645        BoxJSONResponse response = (BoxJSONResponse) request.send();
646        return new Metadata(JsonObject.readFrom(response.getJSON()));
647    }
648
649    /**
650     * Deletes the file properties metadata.
651     */
652    public void deleteMetadata() {
653        this.deleteMetadata(DEFAULT_METADATA_TYPE);
654    }
655
656    /**
657     * Deletes the file metadata of specified template type.
658     * @param typeName the metadata template type name.
659     */
660    public void deleteMetadata(String typeName) {
661        String scope = this.scopeBasedOnType(typeName);
662        URL url = METADATA_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID(), scope, typeName);
663        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "DELETE");
664        request.send();
665    }
666
667    private String scopeBasedOnType(String typeName) {
668        String scope;
669        if (typeName.equals(DEFAULT_METADATA_TYPE)) {
670            scope = GLOBAL_METADATA_SCOPE;
671        } else {
672            scope = ENTERPRISE_METADATA_SCOPE;
673        }
674        return scope;
675    }
676
677    /**
678     * Contains information about a BoxFile.
679     */
680    public class Info extends BoxItem.Info {
681        private String sha1;
682        private String versionNumber;
683        private long commentCount;
684        private EnumSet<Permission> permissions;
685        private String extension;
686        private boolean isPackage;
687        private BoxFileVersion version;
688        private URL previewLink;
689
690        /**
691         * Constructs an empty Info object.
692         */
693        public Info() {
694            super();
695        }
696
697        /**
698         * Constructs an Info object by parsing information from a JSON string.
699         * @param  json the JSON string to parse.
700         */
701        public Info(String json) {
702            super(json);
703        }
704
705        /**
706         * Constructs an Info object using an already parsed JSON object.
707         * @param  jsonObject the parsed JSON object.
708         */
709        Info(JsonObject jsonObject) {
710            super(jsonObject);
711        }
712
713        @Override
714        public BoxFile getResource() {
715            return BoxFile.this;
716        }
717
718        /**
719         * Gets the SHA1 hash of the file.
720         * @return the SHA1 hash of the file.
721         */
722        public String getSha1() {
723            return this.sha1;
724        }
725
726        /**
727         * Gets the current version number of the file.
728         * @return the current version number of the file.
729         */
730        public String getVersionNumber() {
731            return this.versionNumber;
732        }
733
734        /**
735         * Gets the number of comments on the file.
736         * @return the number of comments on the file.
737         */
738        public long getCommentCount() {
739            return this.commentCount;
740        }
741
742        /**
743         * Gets the permissions that the current user has on the file.
744         * @return the permissions that the current user has on the file.
745         */
746        public EnumSet<Permission> getPermissions() {
747            return this.permissions;
748        }
749
750        /**
751         * Gets the extension suffix of the file, excluding the dot.
752         * @return the extension of the file.
753         */
754        public String getExtension() {
755            return this.extension;
756        }
757
758        /**
759         * Gets whether or not the file is an OSX package.
760         * @return true if the file is an OSX package; otherwise false.
761         */
762        public boolean getIsPackage() {
763            return this.isPackage;
764        }
765
766        /**
767         * Gets the current version details of the file.
768         * @return the current version details of the file.
769         */
770        public BoxFileVersion getVersion() {
771            return this.version;
772        }
773
774        /**
775         * Gets the current expiring preview link.
776         * @return the expiring preview link
777         */
778        public URL getPreviewLink() {
779            return this.previewLink;
780        }
781
782        @Override
783        protected void parseJSONMember(JsonObject.Member member) {
784            super.parseJSONMember(member);
785
786            String memberName = member.getName();
787            JsonValue value = member.getValue();
788            if (memberName.equals("sha1")) {
789                this.sha1 = value.asString();
790            } else if (memberName.equals("version_number")) {
791                this.versionNumber = value.asString();
792            } else if (memberName.equals("comment_count")) {
793                this.commentCount = value.asLong();
794            } else if (memberName.equals("permissions")) {
795                this.permissions = this.parsePermissions(value.asObject());
796            } else if (memberName.equals("extension")) {
797                this.extension = value.asString();
798            } else if (memberName.equals("is_package")) {
799                this.isPackage = value.asBoolean();
800            } else if (memberName.equals("file_version")) {
801                this.version = this.parseFileVersion(value.asObject());
802            } else if (memberName.equals("expiring_embed_link")) {
803                try {
804                    String urlString = member.getValue().asObject().get("url").asString();
805                    this.previewLink = new URL(urlString);
806                } catch (MalformedURLException e) {
807                    throw new BoxAPIException("Couldn't parse expiring_embed_link/url for file", e);
808                }
809            }
810        }
811
812        private EnumSet<Permission> parsePermissions(JsonObject jsonObject) {
813            EnumSet<Permission> permissions = EnumSet.noneOf(Permission.class);
814            for (JsonObject.Member member : jsonObject) {
815                JsonValue value = member.getValue();
816                if (value.isNull() || !value.asBoolean()) {
817                    continue;
818                }
819
820                String memberName = member.getName();
821                if (memberName.equals("can_download")) {
822                    permissions.add(Permission.CAN_DOWNLOAD);
823                } else if (memberName.equals("can_upload")) {
824                    permissions.add(Permission.CAN_UPLOAD);
825                } else if (memberName.equals("can_rename")) {
826                    permissions.add(Permission.CAN_RENAME);
827                } else if (memberName.equals("can_delete")) {
828                    permissions.add(Permission.CAN_DELETE);
829                } else if (memberName.equals("can_share")) {
830                    permissions.add(Permission.CAN_SHARE);
831                } else if (memberName.equals("can_set_share_access")) {
832                    permissions.add(Permission.CAN_SET_SHARE_ACCESS);
833                } else if (memberName.equals("can_preview")) {
834                    permissions.add(Permission.CAN_PREVIEW);
835                } else if (memberName.equals("can_comment")) {
836                    permissions.add(Permission.CAN_COMMENT);
837                }
838            }
839
840            return permissions;
841        }
842
843        private BoxFileVersion parseFileVersion(JsonObject jsonObject) {
844            return new BoxFileVersion(BoxFile.this.getAPI(), jsonObject, BoxFile.this.getID());
845        }
846    }
847
848    /**
849     * Enumerates the possible permissions that a user can have on a file.
850     */
851    public enum Permission {
852        /**
853         * The user can download the file.
854         */
855        CAN_DOWNLOAD ("can_download"),
856
857        /**
858         * The user can upload new versions of the file.
859         */
860        CAN_UPLOAD ("can_upload"),
861
862        /**
863         * The user can rename the file.
864         */
865        CAN_RENAME ("can_rename"),
866
867        /**
868         * The user can delete the file.
869         */
870        CAN_DELETE ("can_delete"),
871
872        /**
873         * The user can share the file.
874         */
875        CAN_SHARE ("can_share"),
876
877        /**
878         * The user can set the access level for shared links to the file.
879         */
880        CAN_SET_SHARE_ACCESS ("can_set_share_access"),
881
882        /**
883         * The user can preview the file.
884         */
885        CAN_PREVIEW ("can_preview"),
886
887        /**
888         * The user can comment on the file.
889         */
890        CAN_COMMENT ("can_comment");
891
892        private final String jsonValue;
893
894        private Permission(String jsonValue) {
895            this.jsonValue = jsonValue;
896        }
897
898        static Permission fromJSONValue(String jsonValue) {
899            return Permission.valueOf(jsonValue.toUpperCase());
900        }
901
902        String toJSONValue() {
903            return this.jsonValue;
904        }
905    }
906}