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