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