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.Arrays;
011import java.util.Collection;
012import java.util.Date;
013import java.util.EnumSet;
014import java.util.HashSet;
015import java.util.List;
016import java.util.Map;
017import java.util.Set;
018import java.util.concurrent.TimeUnit;
019
020import com.box.sdk.http.HttpMethod;
021import com.box.sdk.internal.utils.Parsers;
022import com.eclipsesource.json.JsonArray;
023import com.eclipsesource.json.JsonObject;
024import com.eclipsesource.json.JsonValue;
025
026
027/**
028 * Represents an individual file on Box. This class can be used to download a file's contents, upload new versions, and
029 * perform other common file operations (move, copy, delete, etc.).
030 *
031 * <p>Unless otherwise noted, the methods in this class can throw an unchecked {@link BoxAPIException} (unchecked
032 * meaning that the compiler won't force you to handle it) if an error occurs. If you wish to implement custom error
033 * handling for errors related to the Box REST API, you should capture this exception explicitly.
034 */
035@BoxResourceType("file")
036public class BoxFile extends BoxItem {
037
038    /**
039     * An array of all possible file fields that can be requested when calling {@link #getInfo()}.
040     */
041    public static final String[] ALL_FIELDS = {"type", "id", "sequence_id", "etag", "sha1", "name",
042                                               "description", "size", "path_collection", "created_at", "modified_at",
043                                               "trashed_at", "purged_at", "content_created_at", "content_modified_at",
044                                               "created_by", "modified_by", "owned_by", "shared_link", "parent",
045                                               "item_status", "version_number", "comment_count", "permissions", "tags",
046                                               "lock", "extension", "is_package", "file_version", "collections",
047                                               "watermark_info", "metadata", "representations"};
048
049    /**
050     * Used to specify what filetype to request for a file thumbnail.
051     */
052    public enum ThumbnailFileType {
053        /**
054         * PNG image format.
055         */
056        PNG,
057
058        /**
059         * JPG image format.
060         */
061        JPG
062    }
063
064    /**
065     * File URL Template.
066     */
067    public static final URLTemplate FILE_URL_TEMPLATE = new URLTemplate("files/%s");
068    /**
069     * Content URL Template.
070     */
071    public static final URLTemplate CONTENT_URL_TEMPLATE = new URLTemplate("files/%s/content");
072    /**
073     * Versions URL Template.
074     */
075    public static final URLTemplate VERSIONS_URL_TEMPLATE = new URLTemplate("files/%s/versions");
076    /**
077     * Copy URL Template.
078     */
079    public static final URLTemplate COPY_URL_TEMPLATE = new URLTemplate("files/%s/copy");
080    /**
081     * Add Comment URL Template.
082     */
083    public static final URLTemplate ADD_COMMENT_URL_TEMPLATE = new URLTemplate("comments");
084    /**
085     * Get Comments URL Template.
086     */
087    public static final URLTemplate GET_COMMENTS_URL_TEMPLATE = new URLTemplate("files/%s/comments");
088    /**
089     * Metadata URL Template.
090     */
091    public static final URLTemplate METADATA_URL_TEMPLATE = new URLTemplate("files/%s/metadata/%s/%s");
092    /**
093     * Add Task URL Template.
094     */
095    public static final URLTemplate ADD_TASK_URL_TEMPLATE = new URLTemplate("tasks");
096    /**
097     * Get Tasks URL Template.
098     */
099    public static final URLTemplate GET_TASKS_URL_TEMPLATE = new URLTemplate("files/%s/tasks");
100    /**
101     * Get Thumbnail PNG Template.
102     */
103    public static final URLTemplate GET_THUMBNAIL_PNG_TEMPLATE = new URLTemplate("files/%s/thumbnail.png");
104    /**
105     * Get Thumbnail JPG Template.
106     */
107    public static final URLTemplate GET_THUMBNAIL_JPG_TEMPLATE = new URLTemplate("files/%s/thumbnail.jpg");
108    /**
109     * Upload Session URL Template.
110     */
111    public static final URLTemplate UPLOAD_SESSION_URL_TEMPLATE = new URLTemplate("files/%s/upload_sessions");
112    /**
113     * Upload Session Status URL Template.
114     */
115    public static final URLTemplate UPLOAD_SESSION_STATUS_URL_TEMPLATE = new URLTemplate(
116            "files/upload_sessions/%s/status");
117    /**
118     * Abort Upload Session URL Template.
119     */
120    public static final URLTemplate ABORT_UPLOAD_SESSION_URL_TEMPLATE = new URLTemplate("files/upload_sessions/%s");
121    /**
122     * Add Collaborations URL Template.
123     */
124    public static final URLTemplate ADD_COLLABORATION_URL = new URLTemplate("collaborations");
125    /**
126     * Get All File Collaborations URL Template.
127     */
128    public static final URLTemplate GET_ALL_FILE_COLLABORATIONS_URL = new URLTemplate("files/%s/collaborations");
129    private static final int BUFFER_SIZE = 8192;
130    private static final int GET_COLLABORATORS_PAGE_SIZE = 1000;
131
132    /**
133     * Constructs a BoxFile for a file with a given ID.
134     *
135     * @param api the API connection to be used by the file.
136     * @param id  the ID of the file.
137     */
138    public BoxFile(BoxAPIConnection api, String id) {
139        super(api, id);
140    }
141
142    /**
143     * {@inheritDoc}
144     */
145    @Override
146    protected URL getItemURL() {
147        return FILE_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
148    }
149
150    @Override
151    public BoxSharedLink createSharedLink(BoxSharedLink.Access access, Date unshareDate,
152                                          BoxSharedLink.Permissions permissions) {
153
154        BoxSharedLink sharedLink = new BoxSharedLink(access, unshareDate, permissions);
155        Info info = new Info();
156        info.setSharedLink(sharedLink);
157
158        this.updateInfo(info);
159        return info.getSharedLink();
160    }
161
162    /**
163     * Creates new SharedLink for a BoxFile with a password.
164     *
165     * @param access        The access level of the shared link.
166     * @param unshareDate   A specified date to unshare the Box file.
167     * @param permissions   The permissions to set on the shared link for the Box file.
168     * @param password      Password set on the shared link to give access to the Box file.
169     * @return information about the newly created shared link.
170     */
171    public BoxSharedLink createSharedLink(BoxSharedLink.Access access, Date unshareDate,
172                                          BoxSharedLink.Permissions permissions, String password) {
173
174        BoxSharedLink sharedLink = new BoxSharedLink(access, unshareDate, permissions, password);
175        Info info = new Info();
176        info.setSharedLink(sharedLink);
177
178        this.updateInfo(info);
179        return info.getSharedLink();
180    }
181
182    /**
183     * Adds new {@link BoxWebHook} to this {@link BoxFile}.
184     *
185     * @param address  {@link BoxWebHook.Info#getAddress()}
186     * @param triggers {@link BoxWebHook.Info#getTriggers()}
187     * @return created {@link BoxWebHook.Info}
188     */
189    public BoxWebHook.Info addWebHook(URL address, BoxWebHook.Trigger... triggers) {
190        return BoxWebHook.create(this, address, triggers);
191    }
192
193    /**
194     * Adds a comment to this file. The message can contain @mentions by using the string @[userid:username] anywhere
195     * within the message, where userid and username are the ID and username of the person being mentioned.
196     *
197     * @param message the comment's message.
198     * @return information about the newly added comment.
199     * @see <a href="https://developers.box.com/docs/#comments-add-a-comment-to-an-item">the tagged_message field
200     * for including @mentions.</a>
201     */
202    public BoxComment.Info addComment(String message) {
203        JsonObject itemJSON = new JsonObject();
204        itemJSON.add("type", "file");
205        itemJSON.add("id", this.getID());
206
207        JsonObject requestJSON = new JsonObject();
208        requestJSON.add("item", itemJSON);
209        if (BoxComment.messageContainsMention(message)) {
210            requestJSON.add("tagged_message", message);
211        } else {
212            requestJSON.add("message", message);
213        }
214
215        URL url = ADD_COMMENT_URL_TEMPLATE.build(this.getAPI().getBaseURL());
216        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "POST");
217        request.setBody(requestJSON.toString());
218        BoxJSONResponse response = (BoxJSONResponse) request.send();
219        JsonObject responseJSON = JsonObject.readFrom(response.getJSON());
220
221        BoxComment addedComment = new BoxComment(this.getAPI(), responseJSON.get("id").asString());
222        return addedComment.new Info(responseJSON);
223    }
224
225    /**
226     * Adds a new task to this file. The task can have an optional message to include, and a due date.
227     *
228     * @param action  the action the task assignee will be prompted to do.
229     * @param message an optional message to include with the task.
230     * @param dueAt   the day at which this task is due.
231     * @return information about the newly added task.
232     */
233    public BoxTask.Info addTask(BoxTask.Action action, String message, Date dueAt) {
234        JsonObject itemJSON = new JsonObject();
235        itemJSON.add("type", "file");
236        itemJSON.add("id", this.getID());
237
238        JsonObject requestJSON = new JsonObject();
239        requestJSON.add("item", itemJSON);
240        requestJSON.add("action", action.toJSONString());
241
242        if (message != null && !message.isEmpty()) {
243            requestJSON.add("message", message);
244        }
245
246        if (dueAt != null) {
247            requestJSON.add("due_at", BoxDateFormat.format(dueAt));
248        }
249
250        URL url = ADD_TASK_URL_TEMPLATE.build(this.getAPI().getBaseURL());
251        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "POST");
252        request.setBody(requestJSON.toString());
253        BoxJSONResponse response = (BoxJSONResponse) request.send();
254        JsonObject responseJSON = JsonObject.readFrom(response.getJSON());
255
256        BoxTask addedTask = new BoxTask(this.getAPI(), responseJSON.get("id").asString());
257        return addedTask.new Info(responseJSON);
258    }
259
260    /**
261     * Gets an expiring URL for downloading a file directly from Box. This can be user,
262     * for example, for sending as a redirect to a browser to cause the browser
263     * to download the file directly from Box.
264     *
265     * @return the temporary download URL
266     */
267    public URL getDownloadURL() {
268        URL url = CONTENT_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
269        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
270        request.setFollowRedirects(false);
271
272        BoxRedirectResponse response = (BoxRedirectResponse) request.send();
273
274        return response.getRedirectURL();
275    }
276
277    /**
278     * Downloads the contents of this file to a given OutputStream.
279     *
280     * @param output the stream to where the file will be written.
281     */
282    public void download(OutputStream output) {
283        this.download(output, null);
284    }
285
286    /**
287     * Downloads the contents of this file to a given OutputStream while reporting the progress to a ProgressListener.
288     *
289     * @param output   the stream to where the file will be written.
290     * @param listener a listener for monitoring the download's progress.
291     */
292    public void download(OutputStream output, ProgressListener listener) {
293        URL url = CONTENT_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
294        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
295        BoxAPIResponse response = request.send();
296        InputStream input = response.getBody(listener);
297
298        byte[] buffer = new byte[BUFFER_SIZE];
299        try {
300            int n = input.read(buffer);
301            while (n != -1) {
302                output.write(buffer, 0, n);
303                n = input.read(buffer);
304            }
305        } catch (IOException e) {
306            throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e);
307        } finally {
308            response.disconnect();
309        }
310    }
311
312    /**
313     * Downloads a part of this file's contents, starting at specified byte offset.
314     *
315     * @param output the stream to where the file will be written.
316     * @param offset the byte offset at which to start the download.
317     */
318    public void downloadRange(OutputStream output, long offset) {
319        this.downloadRange(output, offset, -1);
320    }
321
322    /**
323     * Downloads a part of this file's contents, starting at rangeStart and stopping at rangeEnd.
324     *
325     * @param output     the stream to where the file will be written.
326     * @param rangeStart the byte offset at which to start the download.
327     * @param rangeEnd   the byte offset at which to stop the download.
328     */
329    public void downloadRange(OutputStream output, long rangeStart, long rangeEnd) {
330        this.downloadRange(output, rangeStart, rangeEnd, null);
331    }
332
333    /**
334     * Downloads a part of this file's contents, starting at rangeStart and stopping at rangeEnd, while reporting the
335     * progress to a ProgressListener.
336     *
337     * @param output     the stream to where the file will be written.
338     * @param rangeStart the byte offset at which to start the download.
339     * @param rangeEnd   the byte offset at which to stop the download.
340     * @param listener   a listener for monitoring the download's progress.
341     */
342    public void downloadRange(OutputStream output, long rangeStart, long rangeEnd, ProgressListener listener) {
343        URL url = CONTENT_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
344        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
345        if (rangeEnd > 0) {
346            request.addHeader("Range", String.format("bytes=%s-%s", Long.toString(rangeStart),
347                    Long.toString(rangeEnd)));
348        } else {
349            request.addHeader("Range", String.format("bytes=%s-", Long.toString(rangeStart)));
350        }
351
352        BoxAPIResponse response = request.send();
353        InputStream input = response.getBody(listener);
354
355        byte[] buffer = new byte[BUFFER_SIZE];
356        try {
357            int n = input.read(buffer);
358            while (n != -1) {
359                output.write(buffer, 0, n);
360                n = input.read(buffer);
361            }
362        } catch (IOException e) {
363            throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e);
364        } finally {
365            response.disconnect();
366        }
367    }
368
369    @Override
370    public BoxFile.Info copy(BoxFolder destination) {
371        return this.copy(destination, null);
372    }
373
374    @Override
375    public BoxFile.Info copy(BoxFolder destination, String newName) {
376        URL url = COPY_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
377
378        JsonObject parent = new JsonObject();
379        parent.add("id", destination.getID());
380
381        JsonObject copyInfo = new JsonObject();
382        copyInfo.add("parent", parent);
383        if (newName != null) {
384            copyInfo.add("name", newName);
385        }
386
387        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "POST");
388        request.setBody(copyInfo.toString());
389        BoxJSONResponse response = (BoxJSONResponse) request.send();
390        JsonObject responseJSON = JsonObject.readFrom(response.getJSON());
391        BoxFile copiedFile = new BoxFile(this.getAPI(), responseJSON.get("id").asString());
392        return copiedFile.new Info(responseJSON);
393    }
394
395    /**
396     * Deletes this file by moving it to the trash.
397     */
398    public void delete() {
399        URL url = FILE_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
400        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "DELETE");
401        BoxAPIResponse response = request.send();
402        response.disconnect();
403    }
404
405    @Override
406    public BoxItem.Info move(BoxFolder destination) {
407        return this.move(destination, null);
408    }
409
410    @Override
411    public BoxItem.Info move(BoxFolder destination, String newName) {
412        URL url = FILE_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
413        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "PUT");
414
415        JsonObject parent = new JsonObject();
416        parent.add("id", destination.getID());
417
418        JsonObject updateInfo = new JsonObject();
419        updateInfo.add("parent", parent);
420        if (newName != null) {
421            updateInfo.add("name", newName);
422        }
423
424        request.setBody(updateInfo.toString());
425        BoxJSONResponse response = (BoxJSONResponse) request.send();
426        JsonObject responseJSON = JsonObject.readFrom(response.getJSON());
427        BoxFile movedFile = new BoxFile(this.getAPI(), responseJSON.get("id").asString());
428        return movedFile.new Info(responseJSON);
429    }
430
431    /**
432     * Renames this file.
433     *
434     * @param newName the new name of the file.
435     */
436    public void rename(String newName) {
437        URL url = FILE_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
438        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "PUT");
439
440        JsonObject updateInfo = new JsonObject();
441        updateInfo.add("name", newName);
442
443        request.setBody(updateInfo.toString());
444        BoxAPIResponse response = request.send();
445        response.disconnect();
446    }
447
448    @Override
449    public BoxFile.Info getInfo() {
450        URL url = FILE_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
451        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
452        BoxJSONResponse response = (BoxJSONResponse) request.send();
453        return new Info(response.getJSON());
454    }
455
456    @Override
457    public BoxFile.Info getInfo(String... fields) {
458        String queryString = new QueryStringBuilder().appendParam("fields", fields).toString();
459        URL url = FILE_URL_TEMPLATE.buildWithQuery(this.getAPI().getBaseURL(), queryString, this.getID());
460
461        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
462        BoxJSONResponse response = (BoxJSONResponse) request.send();
463        return new Info(response.getJSON());
464    }
465
466    /**
467     * Gets information about this item including a specified set of representations.
468     * @see <a href=https://developer.box.com/reference#section-x-rep-hints-header>X-Rep-Hints Header</a>
469     *
470     * @param representationHints hints for representations to be retrieved
471     * @param fields the fields to retrieve.
472     * @return info about this item containing only the specified fields, including representations.
473     */
474    public BoxFile.Info getInfoWithRepresentations(String representationHints, String... fields) {
475        if (representationHints.matches(Representation.X_REP_HINTS_PATTERN)) {
476            //Since the user intends to get representations, add it to fields, even if user has missed it
477            Set<String> fieldsSet = new HashSet<String>(Arrays.asList(fields));
478            fieldsSet.add("representations");
479            String queryString = new QueryStringBuilder().appendParam("fields",
480                fieldsSet.toArray(new String[fieldsSet.size()])).toString();
481            URL url = FILE_URL_TEMPLATE.buildWithQuery(this.getAPI().getBaseURL(), queryString, this.getID());
482
483            BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
484            request.addHeader("X-Rep-Hints", representationHints);
485            BoxJSONResponse response = (BoxJSONResponse) request.send();
486            return new Info(response.getJSON());
487        } else {
488            throw new BoxAPIException("Represention hints is not valid."
489                + " Refer documention on how to construct X-Rep-Hints Header");
490        }
491    }
492
493    /**
494     * Fetches the contents of a file representation and writes them to the provided output stream.
495     * @see <a href=https://developer.box.com/reference#section-x-rep-hints-header>X-Rep-Hints Header</a>
496     * @param representationHint the X-Rep-Hints query for the representation to fetch.
497     * @param output the output stream to write the contents to.
498     */
499    public void getRepresentationContent(String representationHint, OutputStream output) {
500
501        this.getRepresentationContent(representationHint, "", output);
502    }
503
504    /**
505     * Fetches the contents of a file representation with asset path and writes them to the provided output stream.
506     * @see <a href=https://developer.box.com/reference#section-x-rep-hints-header>X-Rep-Hints Header</a>
507     * @param representationHint the X-Rep-Hints query for the representation to fetch.
508     * @param assetPath the path of the asset for representations containing multiple files.
509     * @param output the output stream to write the contents to.
510     */
511    public void getRepresentationContent(String representationHint, String assetPath, OutputStream output) {
512
513        List<Representation> reps = this.getInfoWithRepresentations(representationHint).getRepresentations();
514        if (reps.size() < 1) {
515            throw new BoxAPIException("No matching representations found");
516        }
517        Representation representation = reps.get(0);
518        String repState = representation.getStatus().getState();
519
520        if (repState.equals("viewable") || repState.equals("success")) {
521
522            this.makeRepresentationContentRequest(representation.getContent().getUrlTemplate(),
523                    assetPath, output);
524            return;
525        } else if (repState.equals("pending") || repState.equals("none")) {
526
527            String repContentURLString = null;
528            while (repContentURLString == null) {
529                repContentURLString = this.pollRepInfo(representation.getInfo().getUrl());
530            }
531
532            this.makeRepresentationContentRequest(repContentURLString, assetPath, output);
533            return;
534
535        } else if (repState.equals("error")) {
536
537            throw new BoxAPIException("Representation had error status");
538        } else {
539
540            throw new BoxAPIException("Representation had unknown status");
541        }
542
543    }
544
545    private String pollRepInfo(URL infoURL) {
546
547        BoxAPIRequest infoRequest = new BoxAPIRequest(this.getAPI(), infoURL, HttpMethod.GET);
548        BoxJSONResponse infoResponse = (BoxJSONResponse) infoRequest.send();
549        JsonObject response = infoResponse.getJsonObject();
550
551        Representation rep = new Representation(response);
552
553        String repState = rep.getStatus().getState();
554
555        if (repState.equals("viewable") || repState.equals("success")) {
556
557            return rep.getContent().getUrlTemplate();
558        } else if (repState.equals("pending") || repState.equals("none")) {
559
560            return null;
561
562        } else if (repState.equals("error")) {
563
564            throw new BoxAPIException("Representation had error status");
565        } else {
566
567            throw new BoxAPIException("Representation had unknown status");
568        }
569    }
570
571    private void makeRepresentationContentRequest(String representationURLTemplate, String assetPath,
572                                                  OutputStream output) {
573
574        try {
575
576            URL repURL = new URL(representationURLTemplate.replace("{+asset_path}", assetPath));
577            BoxAPIRequest repContentReq = new BoxAPIRequest(this.getAPI(), repURL, HttpMethod.GET);
578
579            BoxAPIResponse contentResponse = repContentReq.send();
580
581            InputStream input = contentResponse.getBody();
582
583            byte[] buffer = new byte[BUFFER_SIZE];
584            try {
585                int n = input.read(buffer);
586                while (n != -1) {
587                    output.write(buffer, 0, n);
588                    n = input.read(buffer);
589                }
590            } catch (IOException e) {
591                throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e);
592            } finally {
593                contentResponse.disconnect();
594            }
595        } catch (MalformedURLException ex) {
596
597            throw new BoxAPIException("Could not generate representation content URL");
598        }
599    }
600
601    /**
602     * Updates the information about this file with any info fields that have been modified locally.
603     *
604     * <p>The only fields that will be updated are the ones that have been modified locally. For example, the following
605     * code won't update any information (or even send a network request) since none of the info's fields were
606     * changed:</p>
607     *
608     * <pre>BoxFile file = new File(api, id);
609     * BoxFile.Info info = file.getInfo();
610     * file.updateInfo(info);</pre>
611     *
612     * @param info the updated info.
613     */
614    public void updateInfo(BoxFile.Info info) {
615        URL url = FILE_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
616        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "PUT");
617        request.setBody(info.getPendingChanges());
618        BoxJSONResponse response = (BoxJSONResponse) request.send();
619        JsonObject jsonObject = JsonObject.readFrom(response.getJSON());
620        info.update(jsonObject);
621    }
622
623    /**
624     * Gets any previous versions of this file. Note that only users with premium accounts will be able to retrieve
625     * previous versions of their files.
626     *
627     * @return a list of previous file versions.
628     */
629    public Collection<BoxFileVersion> getVersions() {
630        URL url = VERSIONS_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
631        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
632        BoxJSONResponse response = (BoxJSONResponse) request.send();
633
634        JsonObject jsonObject = JsonObject.readFrom(response.getJSON());
635        JsonArray entries = jsonObject.get("entries").asArray();
636        Collection<BoxFileVersion> versions = new ArrayList<BoxFileVersion>();
637        for (JsonValue entry : entries) {
638            versions.add(new BoxFileVersion(this.getAPI(), entry.asObject(), this.getID()));
639        }
640
641        return versions;
642    }
643
644    /**
645     * Checks if the file can be successfully uploaded by using the preflight check.
646     *
647     * @param name     the name to give the uploaded file or null to use existing name.
648     * @param fileSize the size of the file used for account capacity calculations.
649     * @param parentID the ID of the parent folder that the new version is being uploaded to.
650     * @deprecated This method will be removed in future versions of the SDK; use canUploadVersion(String, long) instead
651     */
652    @Deprecated
653    public void canUploadVersion(String name, long fileSize, String parentID) {
654        URL url = CONTENT_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
655        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "OPTIONS");
656
657        JsonObject parent = new JsonObject();
658        parent.add("id", parentID);
659
660        JsonObject preflightInfo = new JsonObject();
661        preflightInfo.add("parent", parent);
662        if (name != null) {
663            preflightInfo.add("name", name);
664        }
665
666        preflightInfo.add("size", fileSize);
667
668        request.setBody(preflightInfo.toString());
669        BoxAPIResponse response = request.send();
670        response.disconnect();
671    }
672
673    /**
674     * Checks if a new version of the file can be uploaded with the specified name.
675     * @param name the new name for the file.
676     * @return whether or not the file version can be uploaded.
677     */
678    public boolean canUploadVersion(String name) {
679        return this.canUploadVersion(name, 0);
680    }
681
682    /**
683     * Checks if a new version of the file can be uploaded with the specified name and size.
684     * @param name the new name for the file.
685     * @param fileSize the size of the new version content in bytes.
686     * @return whether or not the file version can be uploaded.
687     */
688    public boolean canUploadVersion(String name, long fileSize) {
689
690        URL url = CONTENT_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
691        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "OPTIONS");
692
693        JsonObject preflightInfo = new JsonObject();
694        if (name != null) {
695            preflightInfo.add("name", name);
696        }
697
698        preflightInfo.add("size", fileSize);
699
700        request.setBody(preflightInfo.toString());
701        try {
702            BoxAPIResponse response = request.send();
703
704            return response.getResponseCode() == 200;
705        } catch (BoxAPIException ex) {
706
707            if (ex.getResponseCode() >= 400 && ex.getResponseCode() < 500) {
708                // This looks like an error response, menaing the upload would fail
709                return false;
710            } else {
711                // This looks like a network error or server error, rethrow exception
712                throw ex;
713            }
714        }
715    }
716
717    /**
718     * Uploads a new version of this file, replacing the current version. Note that only users with premium accounts
719     * will be able to view and recover previous versions of the file.
720     *
721     * @param fileContent a stream containing the new file contents.
722     * @deprecated use uploadNewVersion() instead.
723     */
724    @Deprecated
725    public void uploadVersion(InputStream fileContent) {
726        this.uploadVersion(fileContent, null);
727    }
728
729    /**
730     * Uploads a new version of this file, replacing the current version. Note that only users with premium accounts
731     * will be able to view and recover previous versions of the file.
732     *
733     * @param fileContent     a stream containing the new file contents.
734     * @param fileContentSHA1 a string containing the SHA1 hash of the new file contents.
735     * @deprecated use uploadNewVersion() instead.
736     */
737    @Deprecated
738    public void uploadVersion(InputStream fileContent, String fileContentSHA1) {
739        this.uploadVersion(fileContent, fileContentSHA1, null);
740    }
741
742    /**
743     * Uploads a new version of this file, replacing the current version. Note that only users with premium accounts
744     * will be able to view and recover previous versions of the file.
745     *
746     * @param fileContent     a stream containing the new file contents.
747     * @param fileContentSHA1 a string containing the SHA1 hash of the new file contents.
748     * @param modified        the date that the new version was modified.
749     * @deprecated use uploadNewVersion() instead.
750     */
751    @Deprecated
752    public void uploadVersion(InputStream fileContent, String fileContentSHA1, Date modified) {
753        this.uploadVersion(fileContent, fileContentSHA1, modified, 0, null);
754    }
755
756    /**
757     * Uploads a new version of this file, replacing the current version, while reporting the progress to a
758     * ProgressListener. Note that only users with premium accounts will be able to view and recover previous versions
759     * of the file.
760     *
761     * @param fileContent a stream containing the new file contents.
762     * @param modified    the date that the new version was modified.
763     * @param fileSize    the size of the file used for determining the progress of the upload.
764     * @param listener    a listener for monitoring the upload's progress.
765     * @deprecated use uploadNewVersion() instead.
766     */
767    @Deprecated
768    public void uploadVersion(InputStream fileContent, Date modified, long fileSize, ProgressListener listener) {
769        this.uploadVersion(fileContent, null, modified, fileSize, listener);
770    }
771
772    /**
773     * Uploads a new version of this file, replacing the current version, while reporting the progress to a
774     * ProgressListener. Note that only users with premium accounts will be able to view and recover previous versions
775     * of the file.
776     *
777     * @param fileContent     a stream containing the new file contents.
778     * @param fileContentSHA1 the SHA1 hash of the file contents. will be sent along in the Content-MD5 header
779     * @param modified        the date that the new version was modified.
780     * @param fileSize        the size of the file used for determining the progress of the upload.
781     * @param listener        a listener for monitoring the upload's progress.
782     * @deprecated use uploadNewVersion() instead.
783     */
784    @Deprecated
785    public void uploadVersion(InputStream fileContent, String fileContentSHA1, Date modified, long fileSize,
786                              ProgressListener listener) {
787        this.uploadNewVersion(fileContent, fileContentSHA1, modified, fileSize, listener);
788        return;
789    }
790
791    /**
792     * Uploads a new version of this file, replacing the current version. Note that only users with premium accounts
793     * will be able to view and recover previous versions of the file.
794     *
795     * @param fileContent a stream containing the new file contents.
796     * @return the uploaded file version.
797     */
798    public BoxFile.Info uploadNewVersion(InputStream fileContent) {
799        return this.uploadNewVersion(fileContent, null);
800    }
801
802    /**
803     * Uploads a new version of this file, replacing the current version. Note that only users with premium accounts
804     * will be able to view and recover previous versions of the file.
805     *
806     * @param fileContent     a stream containing the new file contents.
807     * @param fileContentSHA1 a string containing the SHA1 hash of the new file contents.
808     * @return the uploaded file version.
809     */
810    public BoxFile.Info uploadNewVersion(InputStream fileContent, String fileContentSHA1) {
811        return this.uploadNewVersion(fileContent, fileContentSHA1, null);
812    }
813
814    /**
815     * Uploads a new version of this file, replacing the current version. Note that only users with premium accounts
816     * will be able to view and recover previous versions of the file.
817     *
818     * @param fileContent     a stream containing the new file contents.
819     * @param fileContentSHA1 a string containing the SHA1 hash of the new file contents.
820     * @param modified        the date that the new version was modified.
821     * @return the uploaded file version.
822     */
823    public BoxFile.Info uploadNewVersion(InputStream fileContent, String fileContentSHA1, Date modified) {
824        return this.uploadNewVersion(fileContent, fileContentSHA1, modified, 0, null);
825    }
826
827    /**
828     * Uploads a new version of this file, replacing the current version, while reporting the progress to a
829     * ProgressListener. Note that only users with premium accounts will be able to view and recover previous versions
830     * of the file.
831     *
832     * @param fileContent a stream containing the new file contents.
833     * @param modified    the date that the new version was modified.
834     * @param fileSize    the size of the file used for determining the progress of the upload.
835     * @param listener    a listener for monitoring the upload's progress.
836     * @return the uploaded file version.
837     */
838    public BoxFile.Info uploadNewVersion(InputStream fileContent, Date modified, long fileSize,
839                                         ProgressListener listener) {
840        return this.uploadNewVersion(fileContent, null, modified, fileSize, listener);
841    }
842
843    /**
844     * Uploads a new version of this file, replacing the current version, while reporting the progress to a
845     * ProgressListener. Note that only users with premium accounts will be able to view and recover previous versions
846     * of the file.
847     *
848     * @param fileContent     a stream containing the new file contents.
849     * @param fileContentSHA1 the SHA1 hash of the file contents. will be sent along in the Content-MD5 header
850     * @param modified        the date that the new version was modified.
851     * @param fileSize        the size of the file used for determining the progress of the upload.
852     * @param listener        a listener for monitoring the upload's progress.
853     * @return the uploaded file version.
854     */
855    public BoxFile.Info uploadNewVersion(InputStream fileContent, String fileContentSHA1, Date modified, long fileSize,
856                              ProgressListener listener) {
857        URL uploadURL = CONTENT_URL_TEMPLATE.build(this.getAPI().getBaseUploadURL(), this.getID());
858        BoxMultipartRequest request = new BoxMultipartRequest(getAPI(), uploadURL);
859
860        if (fileSize > 0) {
861            request.setFile(fileContent, "", fileSize);
862        } else {
863            request.setFile(fileContent, "");
864        }
865
866        if (fileContentSHA1 != null) {
867            request.setContentSHA1(fileContentSHA1);
868        }
869
870        if (modified != null) {
871            request.putField("content_modified_at", modified);
872        }
873
874        BoxJSONResponse response;
875        if (listener == null) {
876            response = (BoxJSONResponse) request.send();
877        } else {
878            response = (BoxJSONResponse) request.send(listener);
879        }
880
881        String fileJSON = response.getJsonObject().get("entries").asArray().get(0).toString();
882
883        return new BoxFile.Info(fileJSON);
884    }
885
886    /**
887     * Gets an expiring URL for creating an embedded preview session. The URL will expire after 60 seconds and the
888     * preview session will expire after 60 minutes.
889     *
890     * @return the expiring preview link
891     */
892    public URL getPreviewLink() {
893        BoxFile.Info info = this.getInfo("expiring_embed_link");
894
895        return info.getPreviewLink();
896    }
897
898
899    /**
900     * Retrieves a thumbnail, or smaller image representation, of this file. Sizes of 32x32, 64x64, 128x128,
901     * and 256x256 can be returned in the .png format and sizes of 32x32, 94x94, 160x160, and 320x320 can be returned
902     * in the .jpg format.
903     *
904     * @param fileType  either PNG of JPG
905     * @param minWidth  minimum width
906     * @param minHeight minimum height
907     * @param maxWidth  maximum width
908     * @param maxHeight maximum height
909     * @return the byte array of the thumbnail image
910     */
911    public byte[] getThumbnail(ThumbnailFileType fileType, int minWidth, int minHeight, int maxWidth, int maxHeight) {
912        QueryStringBuilder builder = new QueryStringBuilder();
913        builder.appendParam("min_width", minWidth);
914        builder.appendParam("min_height", minHeight);
915        builder.appendParam("max_width", maxWidth);
916        builder.appendParam("max_height", maxHeight);
917
918        URLTemplate template;
919        if (fileType == ThumbnailFileType.PNG) {
920            template = GET_THUMBNAIL_PNG_TEMPLATE;
921        } else if (fileType == ThumbnailFileType.JPG) {
922            template = GET_THUMBNAIL_JPG_TEMPLATE;
923        } else {
924            throw new BoxAPIException("Unsupported thumbnail file type");
925        }
926        URL url = template.buildWithQuery(this.getAPI().getBaseURL(), builder.toString(), this.getID());
927
928        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
929        BoxAPIResponse response = request.send();
930
931        ByteArrayOutputStream thumbOut = new ByteArrayOutputStream();
932        InputStream body = response.getBody();
933        byte[] buffer = new byte[BUFFER_SIZE];
934        try {
935            int n = body.read(buffer);
936            while (n != -1) {
937                thumbOut.write(buffer, 0, n);
938                n = body.read(buffer);
939            }
940        } catch (IOException e) {
941            throw new BoxAPIException("Error reading thumbnail bytes from response body", e);
942        } finally {
943            response.disconnect();
944        }
945
946        return thumbOut.toByteArray();
947    }
948
949    /**
950     * Gets a list of any comments on this file.
951     *
952     * @return a list of comments on this file.
953     */
954    public List<BoxComment.Info> getComments() {
955        URL url = GET_COMMENTS_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
956        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
957        BoxJSONResponse response = (BoxJSONResponse) request.send();
958        JsonObject responseJSON = JsonObject.readFrom(response.getJSON());
959
960        int totalCount = responseJSON.get("total_count").asInt();
961        List<BoxComment.Info> comments = new ArrayList<BoxComment.Info>(totalCount);
962        JsonArray entries = responseJSON.get("entries").asArray();
963        for (JsonValue value : entries) {
964            JsonObject commentJSON = value.asObject();
965            BoxComment comment = new BoxComment(this.getAPI(), commentJSON.get("id").asString());
966            BoxComment.Info info = comment.new Info(commentJSON);
967            comments.add(info);
968        }
969
970        return comments;
971    }
972
973    /**
974     * Gets a list of any tasks on this file with requested fields.
975     *
976     * @param fields optional fields to retrieve for this task.
977     * @return a list of tasks on this file.
978     */
979    public List<BoxTask.Info> getTasks(String... fields) {
980        QueryStringBuilder builder = new QueryStringBuilder();
981        if (fields.length > 0) {
982            builder.appendParam("fields", fields).toString();
983        }
984        URL url = GET_TASKS_URL_TEMPLATE.buildWithQuery(this.getAPI().getBaseURL(), builder.toString(), this.getID());
985        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
986        BoxJSONResponse response = (BoxJSONResponse) request.send();
987        JsonObject responseJSON = JsonObject.readFrom(response.getJSON());
988
989        int totalCount = responseJSON.get("total_count").asInt();
990        List<BoxTask.Info> tasks = new ArrayList<BoxTask.Info>(totalCount);
991        JsonArray entries = responseJSON.get("entries").asArray();
992        for (JsonValue value : entries) {
993            JsonObject taskJSON = value.asObject();
994            BoxTask task = new BoxTask(this.getAPI(), taskJSON.get("id").asString());
995            BoxTask.Info info = task.new Info(taskJSON);
996            tasks.add(info);
997        }
998
999        return tasks;
1000    }
1001
1002    /**
1003     * Creates metadata on this file in the global properties template.
1004     *
1005     * @param metadata The new metadata values.
1006     * @return the metadata returned from the server.
1007     */
1008    public Metadata createMetadata(Metadata metadata) {
1009        return this.createMetadata(Metadata.DEFAULT_METADATA_TYPE, metadata);
1010    }
1011
1012    /**
1013     * Creates metadata on this file in the specified template type.
1014     *
1015     * @param typeName the metadata template type name.
1016     * @param metadata the new metadata values.
1017     * @return the metadata returned from the server.
1018     */
1019    public Metadata createMetadata(String typeName, Metadata metadata) {
1020        String scope = Metadata.scopeBasedOnType(typeName);
1021        return this.createMetadata(typeName, scope, metadata);
1022    }
1023
1024    /**
1025     * Creates metadata on this file in the specified template type.
1026     *
1027     * @param typeName the metadata template type name.
1028     * @param scope    the metadata scope (global or enterprise).
1029     * @param metadata the new metadata values.
1030     * @return the metadata returned from the server.
1031     */
1032    public Metadata createMetadata(String typeName, String scope, Metadata metadata) {
1033        URL url = METADATA_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID(), scope, typeName);
1034        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "POST");
1035        request.addHeader("Content-Type", "application/json");
1036        request.setBody(metadata.toString());
1037        BoxJSONResponse response = (BoxJSONResponse) request.send();
1038        return new Metadata(JsonObject.readFrom(response.getJSON()));
1039    }
1040
1041    /**
1042     * Sets the provided metadata on the file, overwriting any existing metadata keys already present.
1043     *
1044     * @param templateName the name of the metadata template.
1045     * @param scope        the scope of the template (usually "global" or "enterprise").
1046     * @param metadata     the new metadata values.
1047     * @return the metadata returned from the server.
1048     */
1049    public Metadata setMetadata(String templateName, String scope, Metadata metadata) {
1050        Metadata metadataValue = null;
1051
1052        try {
1053            metadataValue = this.createMetadata(templateName, scope, metadata);
1054        } catch (BoxAPIException e) {
1055            if (e.getResponseCode() == 409) {
1056                Metadata metadataToUpdate = new Metadata(scope, templateName);
1057                for (JsonValue value : metadata.getOperations()) {
1058                    if (value.asObject().get("value").isNumber()) {
1059                        metadataToUpdate.add(value.asObject().get("path").asString(),
1060                                value.asObject().get("value").asFloat());
1061                    } else if (value.asObject().get("value").isString()) {
1062                        metadataToUpdate.add(value.asObject().get("path").asString(),
1063                                value.asObject().get("value").asString());
1064                    } else if (value.asObject().get("value").isArray()) {
1065                        ArrayList<String> list = new ArrayList<String>();
1066                        for (JsonValue jsonValue : value.asObject().get("value").asArray()) {
1067                            list.add(jsonValue.asString());
1068                        }
1069                        metadataToUpdate.add(value.asObject().get("path").asString(), list);
1070                    }
1071                }
1072                metadataValue = this.updateMetadata(metadataToUpdate);
1073            }
1074        }
1075
1076        return metadataValue;
1077    }
1078
1079    /**
1080     * Adds a metadata classification to the specified file.
1081     *
1082     * @param classificationType the metadata classification type.
1083     * @return the metadata classification type added to the file.
1084     */
1085    public String addClassification(String classificationType) {
1086        Metadata metadata = new Metadata().add(Metadata.CLASSIFICATION_KEY, classificationType);
1087        Metadata classification = this.createMetadata(Metadata.CLASSIFICATION_TEMPLATE_KEY,
1088                "enterprise", metadata);
1089
1090        return classification.getString(Metadata.CLASSIFICATION_KEY);
1091    }
1092
1093    /**
1094     * Updates a metadata classification on the specified file.
1095     *
1096     * @param classificationType the metadata classification type.
1097     * @return the new metadata classification type updated on the file.
1098     */
1099    public String updateClassification(String classificationType) {
1100        Metadata metadata = new Metadata("enterprise", Metadata.CLASSIFICATION_TEMPLATE_KEY);
1101        metadata.add("/Box__Security__Classification__Key", classificationType);
1102        Metadata classification = this.updateMetadata(metadata);
1103
1104        return classification.getString(Metadata.CLASSIFICATION_KEY);
1105    }
1106
1107    /**
1108     * Attempts to add classification to a file. If classification already exists then do update.
1109     *
1110     * @param classificationType the metadata classification type.
1111     * @return the metadata classification type on the file.
1112     */
1113    public String setClassification(String classificationType) {
1114        Metadata metadata = new Metadata().add(Metadata.CLASSIFICATION_KEY, classificationType);
1115        Metadata classification = null;
1116
1117        try {
1118            classification = this.createMetadata(Metadata.CLASSIFICATION_TEMPLATE_KEY, "enterprise", metadata);
1119        } catch (BoxAPIException e) {
1120            if (e.getResponseCode() == 409) {
1121                metadata = new Metadata("enterprise", Metadata.CLASSIFICATION_TEMPLATE_KEY);
1122                metadata.replace(Metadata.CLASSIFICATION_KEY, classificationType);
1123                classification = this.updateMetadata(metadata);
1124            } else {
1125                throw e;
1126            }
1127        }
1128
1129        return classification.getString(Metadata.CLASSIFICATION_KEY);
1130    }
1131
1132    /**
1133     * Gets the classification type for the specified file.
1134     *
1135     * @return the metadata classification type on the file.
1136     */
1137    public String getClassification() {
1138        Metadata metadata;
1139        try {
1140            metadata = this.getMetadata(Metadata.CLASSIFICATION_TEMPLATE_KEY);
1141
1142        } catch (BoxAPIException e) {
1143            JsonObject responseObject = JsonObject.readFrom(e.getResponse());
1144            String code = responseObject.get("code").asString();
1145
1146            if (e.getResponseCode() == 404 && code.equals("instance_not_found")) {
1147                return null;
1148            } else {
1149                throw e;
1150            }
1151        }
1152
1153        return metadata.getString(Metadata.CLASSIFICATION_KEY);
1154    }
1155
1156    /**
1157     * Deletes the classification on the file.
1158     */
1159    public void deleteClassification() {
1160        this.deleteMetadata(Metadata.CLASSIFICATION_TEMPLATE_KEY, "enterprise");
1161    }
1162
1163    /**
1164     * Locks a file.
1165     *
1166     * @return the lock returned from the server.
1167     */
1168    public BoxLock lock() {
1169        return this.lock(null, false);
1170    }
1171
1172    /**
1173     * Locks a file.
1174     *
1175     * @param isDownloadPrevented is downloading of file prevented when locked.
1176     * @return the lock returned from the server.
1177     */
1178    public BoxLock lock(boolean isDownloadPrevented) {
1179        return this.lock(null, isDownloadPrevented);
1180    }
1181
1182    /**
1183     * Locks a file.
1184     *
1185     * @param expiresAt expiration date of the lock.
1186     * @return the lock returned from the server.
1187     */
1188    public BoxLock lock(Date expiresAt) {
1189        return this.lock(expiresAt, false);
1190    }
1191
1192    /**
1193     * Locks a file.
1194     *
1195     * @param expiresAt           expiration date of the lock.
1196     * @param isDownloadPrevented is downloading of file prevented when locked.
1197     * @return the lock returned from the server.
1198     */
1199    public BoxLock lock(Date expiresAt, boolean isDownloadPrevented) {
1200        String queryString = new QueryStringBuilder().appendParam("fields", "lock").toString();
1201        URL url = FILE_URL_TEMPLATE.buildWithQuery(this.getAPI().getBaseURL(), queryString, this.getID());
1202        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "PUT");
1203
1204        JsonObject lockConfig = new JsonObject();
1205        lockConfig.add("type", "lock");
1206        if (expiresAt != null) {
1207            lockConfig.add("expires_at", BoxDateFormat.format(expiresAt));
1208        }
1209        lockConfig.add("is_download_prevented", isDownloadPrevented);
1210
1211        JsonObject requestJSON = new JsonObject();
1212        requestJSON.add("lock", lockConfig);
1213        request.setBody(requestJSON.toString());
1214
1215        BoxJSONResponse response = (BoxJSONResponse) request.send();
1216
1217        JsonObject responseJSON = JsonObject.readFrom(response.getJSON());
1218        JsonValue lockValue = responseJSON.get("lock");
1219        JsonObject lockJSON = JsonObject.readFrom(lockValue.toString());
1220
1221        return new BoxLock(lockJSON, this.getAPI());
1222    }
1223
1224    /**
1225     * Unlocks a file.
1226     */
1227    public void unlock() {
1228        String queryString = new QueryStringBuilder().appendParam("fields", "lock").toString();
1229        URL url = FILE_URL_TEMPLATE.buildWithQuery(this.getAPI().getBaseURL(), queryString, this.getID());
1230        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "PUT");
1231
1232        JsonObject lockObject = new JsonObject();
1233        lockObject.add("lock", JsonObject.NULL);
1234
1235        request.setBody(lockObject.toString());
1236        request.send();
1237    }
1238
1239    /**
1240     * Used to retrieve all metadata associated with the file.
1241     *
1242     * @param fields the optional fields to retrieve.
1243     * @return An iterable of metadata instances associated with the file.
1244     */
1245    public Iterable<Metadata> getAllMetadata(String... fields) {
1246        return Metadata.getAllMetadata(this, fields);
1247    }
1248
1249    /**
1250     * Gets the file properties metadata.
1251     *
1252     * @return the metadata returned from the server.
1253     */
1254    public Metadata getMetadata() {
1255        return this.getMetadata(Metadata.DEFAULT_METADATA_TYPE);
1256    }
1257
1258    /**
1259     * Gets the file metadata of specified template type.
1260     *
1261     * @param typeName the metadata template type name.
1262     * @return the metadata returned from the server.
1263     */
1264    public Metadata getMetadata(String typeName) {
1265        String scope = Metadata.scopeBasedOnType(typeName);
1266        return this.getMetadata(typeName, scope);
1267    }
1268
1269    /**
1270     * Gets the file metadata of specified template type.
1271     *
1272     * @param typeName the metadata template type name.
1273     * @param scope    the metadata scope (global or enterprise).
1274     * @return the metadata returned from the server.
1275     */
1276    public Metadata getMetadata(String typeName, String scope) {
1277        URL url = METADATA_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID(), scope, typeName);
1278        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
1279        BoxJSONResponse response = (BoxJSONResponse) request.send();
1280        return new Metadata(JsonObject.readFrom(response.getJSON()));
1281    }
1282
1283    /**
1284     * Updates the file metadata.
1285     *
1286     * @param metadata the new metadata values.
1287     * @return the metadata returned from the server.
1288     */
1289    public Metadata updateMetadata(Metadata metadata) {
1290        String scope;
1291        if (metadata.getScope().equals(Metadata.GLOBAL_METADATA_SCOPE)) {
1292            scope = Metadata.GLOBAL_METADATA_SCOPE;
1293        } else {
1294            scope = Metadata.ENTERPRISE_METADATA_SCOPE;
1295        }
1296
1297        URL url = METADATA_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID(),
1298                scope, metadata.getTemplateName());
1299        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "PUT");
1300        request.addHeader("Content-Type", "application/json-patch+json");
1301        request.setBody(metadata.getPatch());
1302        BoxJSONResponse response = (BoxJSONResponse) request.send();
1303        return new Metadata(JsonObject.readFrom(response.getJSON()));
1304    }
1305
1306    /**
1307     * Deletes the file properties metadata.
1308     */
1309    public void deleteMetadata() {
1310        this.deleteMetadata(Metadata.DEFAULT_METADATA_TYPE);
1311    }
1312
1313    /**
1314     * Deletes the file metadata of specified template type.
1315     *
1316     * @param typeName the metadata template type name.
1317     */
1318    public void deleteMetadata(String typeName) {
1319        String scope = Metadata.scopeBasedOnType(typeName);
1320        this.deleteMetadata(typeName, scope);
1321    }
1322
1323    /**
1324     * Deletes the file metadata of specified template type.
1325     *
1326     * @param typeName the metadata template type name.
1327     * @param scope    the metadata scope (global or enterprise).
1328     */
1329    public void deleteMetadata(String typeName, String scope) {
1330        URL url = METADATA_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID(), scope, typeName);
1331        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "DELETE");
1332        request.send();
1333    }
1334
1335    /**
1336     * Used to retrieve the watermark for the file.
1337     * If the file does not have a watermark applied to it, a 404 Not Found will be returned by API.
1338     *
1339     * @param fields the fields to retrieve.
1340     * @return the watermark associated with the file.
1341     */
1342    public BoxWatermark getWatermark(String... fields) {
1343        return this.getWatermark(FILE_URL_TEMPLATE, fields);
1344    }
1345
1346    /**
1347     * Used to apply or update the watermark for the file.
1348     *
1349     * @return the watermark associated with the file.
1350     */
1351    public BoxWatermark applyWatermark() {
1352        return this.applyWatermark(FILE_URL_TEMPLATE, BoxWatermark.WATERMARK_DEFAULT_IMPRINT);
1353    }
1354
1355    /**
1356     * Removes a watermark from the file.
1357     * If the file did not have a watermark applied to it, a 404 Not Found will be returned by API.
1358     */
1359    public void removeWatermark() {
1360        this.removeWatermark(FILE_URL_TEMPLATE);
1361    }
1362
1363    /**
1364     * {@inheritDoc}
1365     */
1366    @Override
1367    public BoxFile.Info setCollections(BoxCollection... collections) {
1368        JsonArray jsonArray = new JsonArray();
1369        for (BoxCollection collection : collections) {
1370            JsonObject collectionJSON = new JsonObject();
1371            collectionJSON.add("id", collection.getID());
1372            jsonArray.add(collectionJSON);
1373        }
1374        JsonObject infoJSON = new JsonObject();
1375        infoJSON.add("collections", jsonArray);
1376
1377        String queryString = new QueryStringBuilder().appendParam("fields", ALL_FIELDS).toString();
1378        URL url = FILE_URL_TEMPLATE.buildWithQuery(this.getAPI().getBaseURL(), queryString, this.getID());
1379        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "PUT");
1380        request.setBody(infoJSON.toString());
1381        BoxJSONResponse response = (BoxJSONResponse) request.send();
1382        JsonObject jsonObject = JsonObject.readFrom(response.getJSON());
1383        return new Info(jsonObject);
1384    }
1385
1386    /**
1387     * Creates an upload session to create a new version of a file in chunks.
1388     * This will first verify that the version can be created and then open a session for uploading pieces of the file.
1389     * @param fileSize the size of the file that will be uploaded.
1390     * @return the created upload session instance.
1391     */
1392    public BoxFileUploadSession.Info createUploadSession(long fileSize) {
1393        URL url = UPLOAD_SESSION_URL_TEMPLATE.build(this.getAPI().getBaseUploadURL(), this.getID());
1394
1395        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "POST");
1396        request.addHeader("Content-Type", "application/json");
1397
1398        JsonObject body = new JsonObject();
1399        body.add("file_size", fileSize);
1400        request.setBody(body.toString());
1401
1402        BoxJSONResponse response = (BoxJSONResponse) request.send();
1403        JsonObject jsonObject = JsonObject.readFrom(response.getJSON());
1404
1405        String sessionId = jsonObject.get("id").asString();
1406        BoxFileUploadSession session = new BoxFileUploadSession(this.getAPI(), sessionId);
1407        return session.new Info(jsonObject);
1408    }
1409
1410    /**
1411     * Creates a new version of a file.
1412     * @param inputStream the stream instance that contains the data.
1413     * @param fileSize the size of the file that will be uploaded.
1414     * @return the created file instance.
1415     * @throws InterruptedException when a thread execution is interrupted.
1416     * @throws IOException when reading a stream throws exception.
1417     */
1418    public BoxFile.Info uploadLargeFile(InputStream inputStream, long fileSize)
1419        throws InterruptedException, IOException {
1420        URL url = UPLOAD_SESSION_URL_TEMPLATE.build(this.getAPI().getBaseUploadURL(), this.getID());
1421        return new LargeFileUpload().upload(this.getAPI(), inputStream, url, fileSize);
1422    }
1423
1424    /**
1425     * Creates a new version of a file using specified number of parallel http connections.
1426     * @param inputStream the stream instance that contains the data.
1427     * @param fileSize the size of the file that will be uploaded.
1428     * @param nParallelConnections number of parallel http connections to use
1429     * @param timeOut time to wait before killing the job
1430     * @param unit time unit for the time wait value
1431     * @return the created file instance.
1432     * @throws InterruptedException when a thread execution is interrupted.
1433     * @throws IOException when reading a stream throws exception.
1434     */
1435    public BoxFile.Info uploadLargeFile(InputStream inputStream, long fileSize,
1436                                        int nParallelConnections, long timeOut, TimeUnit unit)
1437        throws InterruptedException, IOException {
1438        URL url = UPLOAD_SESSION_URL_TEMPLATE.build(this.getAPI().getBaseUploadURL(), this.getID());
1439        return new LargeFileUpload(nParallelConnections, timeOut, unit)
1440            .upload(this.getAPI(), inputStream, url, fileSize);
1441    }
1442
1443    private BoxCollaboration.Info collaborate(JsonObject accessibleByField, BoxCollaboration.Role role,
1444                                              Boolean notify, Boolean canViewPath) {
1445
1446        JsonObject itemField = new JsonObject();
1447        itemField.add("id", this.getID());
1448        itemField.add("type", "file");
1449
1450        return BoxCollaboration.create(this.getAPI(), accessibleByField, itemField, role, notify, canViewPath);
1451    }
1452
1453    /**
1454     * Adds a collaborator to this file.
1455     *
1456     * @param collaborator the collaborator to add.
1457     * @param role         the role of the collaborator.
1458     * @param notify       determines if the user (or all the users in the group) will receive email notifications.
1459     * @param canViewPath  whether view path collaboration feature is enabled or not.
1460     * @return info about the new collaboration.
1461     */
1462    public BoxCollaboration.Info collaborate(BoxCollaborator collaborator, BoxCollaboration.Role role,
1463                                             Boolean notify, Boolean canViewPath) {
1464        JsonObject accessibleByField = new JsonObject();
1465        accessibleByField.add("id", collaborator.getID());
1466
1467        if (collaborator instanceof BoxUser) {
1468            accessibleByField.add("type", "user");
1469        } else if (collaborator instanceof BoxGroup) {
1470            accessibleByField.add("type", "group");
1471        } else {
1472            throw new IllegalArgumentException("The given collaborator is of an unknown type.");
1473        }
1474        return this.collaborate(accessibleByField, role, notify, canViewPath);
1475    }
1476
1477
1478    /**
1479     * Adds a collaborator to this folder. An email will be sent to the collaborator if they don't already have a Box
1480     * account.
1481     *
1482     * @param email the email address of the collaborator to add.
1483     * @param role  the role of the collaborator.
1484     * @param notify       determines if the user (or all the users in the group) will receive email notifications.
1485     * @param canViewPath  whether view path collaboration feature is enabled or not.
1486     * @return info about the new collaboration.
1487     */
1488    public BoxCollaboration.Info collaborate(String email, BoxCollaboration.Role role,
1489                                             Boolean notify, Boolean canViewPath) {
1490        JsonObject accessibleByField = new JsonObject();
1491        accessibleByField.add("login", email);
1492        accessibleByField.add("type", "user");
1493
1494        return this.collaborate(accessibleByField, role, notify, canViewPath);
1495    }
1496
1497    /**
1498     * Used to retrieve all collaborations associated with the item.
1499     *
1500     * @param fields the optional fields to retrieve.
1501     * @return An iterable of metadata instances associated with the item.
1502     */
1503    public BoxResourceIterable<BoxCollaboration.Info> getAllFileCollaborations(String... fields) {
1504        return BoxCollaboration.getAllFileCollaborations(this.getAPI(), this.getID(),
1505                GET_COLLABORATORS_PAGE_SIZE, fields);
1506
1507    }
1508
1509    /**
1510     * Contains information about a BoxFile.
1511     */
1512    public class Info extends BoxItem.Info {
1513        private String sha1;
1514        private String versionNumber;
1515        private long commentCount;
1516        private EnumSet<Permission> permissions;
1517        private String extension;
1518        private boolean isPackage;
1519        private BoxFileVersion version;
1520        private URL previewLink;
1521        private BoxLock lock;
1522        private boolean isWatermarked;
1523        private Boolean isExternallyOwned;
1524        private JsonObject metadata;
1525        private Map<String, Map<String, Metadata>> metadataMap;
1526        private List<Representation> representations;
1527
1528        /**
1529         * Constructs an empty Info object.
1530         */
1531        public Info() {
1532            super();
1533        }
1534
1535        /**
1536         * Constructs an Info object by parsing information from a JSON string.
1537         *
1538         * @param json the JSON string to parse.
1539         */
1540        public Info(String json) {
1541            super(json);
1542        }
1543
1544        /**
1545         * Constructs an Info object using an already parsed JSON object.
1546         *
1547         * @param jsonObject the parsed JSON object.
1548         */
1549        public Info(JsonObject jsonObject) {
1550            super(jsonObject);
1551        }
1552
1553        @Override
1554        public BoxFile getResource() {
1555            return BoxFile.this;
1556        }
1557
1558        /**
1559         * Gets the SHA1 hash of the file.
1560         *
1561         * @return the SHA1 hash of the file.
1562         */
1563        public String getSha1() {
1564            return this.sha1;
1565        }
1566
1567        /**
1568         * Gets the lock of the file.
1569         *
1570         * @return the lock of the file.
1571         */
1572        public BoxLock getLock() {
1573            return this.lock;
1574        }
1575
1576        /**
1577         * Gets the current version number of the file.
1578         *
1579         * @return the current version number of the file.
1580         */
1581        public String getVersionNumber() {
1582            return this.versionNumber;
1583        }
1584
1585        /**
1586         * Gets the number of comments on the file.
1587         *
1588         * @return the number of comments on the file.
1589         */
1590        public long getCommentCount() {
1591            return this.commentCount;
1592        }
1593
1594        /**
1595         * Gets the permissions that the current user has on the file.
1596         *
1597         * @return the permissions that the current user has on the file.
1598         */
1599        public EnumSet<Permission> getPermissions() {
1600            return this.permissions;
1601        }
1602
1603        /**
1604         * Gets the extension suffix of the file, excluding the dot.
1605         *
1606         * @return the extension of the file.
1607         */
1608        public String getExtension() {
1609            return this.extension;
1610        }
1611
1612        /**
1613         * Gets whether or not the file is an OSX package.
1614         *
1615         * @return true if the file is an OSX package; otherwise false.
1616         */
1617        public boolean getIsPackage() {
1618            return this.isPackage;
1619        }
1620
1621        /**
1622         * Gets the current version details of the file.
1623         *
1624         * @return the current version details of the file.
1625         */
1626        public BoxFileVersion getVersion() {
1627            return this.version;
1628        }
1629
1630        /**
1631         * Gets the current expiring preview link.
1632         *
1633         * @return the expiring preview link
1634         */
1635        public URL getPreviewLink() {
1636            return this.previewLink;
1637        }
1638
1639        /**
1640         * Gets flag indicating whether this file is Watermarked.
1641         *
1642         * @return whether the file is watermarked or not
1643         */
1644        public boolean getIsWatermarked() {
1645            return this.isWatermarked;
1646        }
1647
1648        /**
1649         * Gets the metadata on this file associated with a specified scope and template.
1650         * Makes an attempt to get metadata that was retrieved using getInfo(String ...) method. If no result is found
1651         * then makes an API call to get metadata
1652         * @param   templateName    the metadata template type name.
1653         * @param   scope           the scope of the template (usually "global" or "enterprise").
1654         * @return                  the metadata returned from the server.
1655         */
1656        public Metadata getMetadata(String templateName, String scope) {
1657            try {
1658                return this.metadataMap.get(scope).get(templateName);
1659            } catch (NullPointerException e) {
1660                return null;
1661            }
1662        }
1663
1664        /**
1665         * Returns the field for indicating whether a file is owned by a user outside the enterprise.
1666         * @return indicator for whether or not the file is owned by a user outside the enterprise.
1667         */
1668        public boolean getIsExternallyOwned() {
1669            return this.isExternallyOwned;
1670        }
1671
1672        /**
1673         * Get file's representations.
1674         * @return list of representations
1675         */
1676        public List<Representation> getRepresentations() {
1677            return this.representations;
1678        }
1679
1680        @Override
1681        protected void parseJSONMember(JsonObject.Member member) {
1682            super.parseJSONMember(member);
1683
1684            String memberName = member.getName();
1685            JsonValue value = member.getValue();
1686            if (memberName.equals("sha1")) {
1687                this.sha1 = value.asString();
1688            } else if (memberName.equals("version_number")) {
1689                this.versionNumber = value.asString();
1690            } else if (memberName.equals("comment_count")) {
1691                this.commentCount = value.asLong();
1692            } else if (memberName.equals("permissions")) {
1693                this.permissions = this.parsePermissions(value.asObject());
1694            } else if (memberName.equals("extension")) {
1695                this.extension = value.asString();
1696            } else if (memberName.equals("is_package")) {
1697                this.isPackage = value.asBoolean();
1698            } else if (memberName.equals("is_externally_owned")) {
1699                this.isExternallyOwned = value.asBoolean();
1700            } else if (memberName.equals("file_version")) {
1701                this.version = this.parseFileVersion(value.asObject());
1702            } else if (memberName.equals("expiring_embed_link")) {
1703                try {
1704                    String urlString = member.getValue().asObject().get("url").asString();
1705                    this.previewLink = new URL(urlString);
1706                } catch (MalformedURLException e) {
1707                    throw new BoxAPIException("Couldn't parse expiring_embed_link/url for file", e);
1708                }
1709            } else if (memberName.equals("lock")) {
1710                if (value.isNull()) {
1711                    this.lock = null;
1712                } else {
1713                    this.lock = new BoxLock(value.asObject(), BoxFile.this.getAPI());
1714                }
1715            } else if (memberName.equals("watermark_info")) {
1716                JsonObject jsonObject = value.asObject();
1717                this.isWatermarked = jsonObject.get("is_watermarked").asBoolean();
1718            } else if (memberName.equals("metadata")) {
1719                JsonObject jsonObject = value.asObject();
1720                this.metadataMap = Parsers.parseAndPopulateMetadataMap(jsonObject);
1721            } else if (memberName.equals("representations")) {
1722                JsonObject jsonObject = value.asObject();
1723                this.representations = Parsers.parseRepresentations(jsonObject);
1724            }
1725        }
1726
1727        private EnumSet<Permission> parsePermissions(JsonObject jsonObject) {
1728            EnumSet<Permission> permissions = EnumSet.noneOf(Permission.class);
1729            for (JsonObject.Member member : jsonObject) {
1730                JsonValue value = member.getValue();
1731                if (value.isNull() || !value.asBoolean()) {
1732                    continue;
1733                }
1734
1735                String memberName = member.getName();
1736                if (memberName.equals("can_download")) {
1737                    permissions.add(Permission.CAN_DOWNLOAD);
1738                } else if (memberName.equals("can_upload")) {
1739                    permissions.add(Permission.CAN_UPLOAD);
1740                } else if (memberName.equals("can_rename")) {
1741                    permissions.add(Permission.CAN_RENAME);
1742                } else if (memberName.equals("can_delete")) {
1743                    permissions.add(Permission.CAN_DELETE);
1744                } else if (memberName.equals("can_share")) {
1745                    permissions.add(Permission.CAN_SHARE);
1746                } else if (memberName.equals("can_set_share_access")) {
1747                    permissions.add(Permission.CAN_SET_SHARE_ACCESS);
1748                } else if (memberName.equals("can_preview")) {
1749                    permissions.add(Permission.CAN_PREVIEW);
1750                } else if (memberName.equals("can_comment")) {
1751                    permissions.add(Permission.CAN_COMMENT);
1752                }
1753            }
1754
1755            return permissions;
1756        }
1757
1758        private BoxFileVersion parseFileVersion(JsonObject jsonObject) {
1759            return new BoxFileVersion(BoxFile.this.getAPI(), jsonObject, BoxFile.this.getID());
1760        }
1761    }
1762
1763    /**
1764     * Enumerates the possible permissions that a user can have on a file.
1765     */
1766    public enum Permission {
1767        /**
1768         * The user can download the file.
1769         */
1770        CAN_DOWNLOAD("can_download"),
1771
1772        /**
1773         * The user can upload new versions of the file.
1774         */
1775        CAN_UPLOAD("can_upload"),
1776
1777        /**
1778         * The user can rename the file.
1779         */
1780        CAN_RENAME("can_rename"),
1781
1782        /**
1783         * The user can delete the file.
1784         */
1785        CAN_DELETE("can_delete"),
1786
1787        /**
1788         * The user can share the file.
1789         */
1790        CAN_SHARE("can_share"),
1791
1792        /**
1793         * The user can set the access level for shared links to the file.
1794         */
1795        CAN_SET_SHARE_ACCESS("can_set_share_access"),
1796
1797        /**
1798         * The user can preview the file.
1799         */
1800        CAN_PREVIEW("can_preview"),
1801
1802        /**
1803         * The user can comment on the file.
1804         */
1805        CAN_COMMENT("can_comment");
1806
1807        private final String jsonValue;
1808
1809        private Permission(String jsonValue) {
1810            this.jsonValue = jsonValue;
1811        }
1812
1813        static Permission fromJSONValue(String jsonValue) {
1814            return Permission.valueOf(jsonValue.toUpperCase());
1815        }
1816
1817        String toJSONValue() {
1818            return this.jsonValue;
1819        }
1820    }
1821
1822}