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