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
509        List<Representation> reps = this.getInfoWithRepresentations(representationHint).getRepresentations();
510        if (reps.size() < 1) {
511            throw new BoxAPIException("No matching representations found for requested '" + representationHint
512                + "' hint");
513        }
514        Representation representation = reps.get(0);
515        String repState = representation.getStatus().getState();
516
517        switch (repState) {
518            case "viewable":
519            case "success":
520                this.makeRepresentationContentRequest(representation.getContent().getUrlTemplate(), assetPath, output);
521                break;
522            case "pending":
523            case "none":
524
525                String repContentURLString = null;
526                while (repContentURLString == null) {
527                    repContentURLString = this.pollRepInfo(representation.getInfo().getUrl());
528                    try {
529                        Thread.sleep(100);
530                    } catch (InterruptedException e) {
531                        throw new RuntimeException(e);
532                    }
533                }
534
535                this.makeRepresentationContentRequest(repContentURLString, assetPath, output);
536                break;
537            case "error":
538                throw new BoxAPIException("Representation had error status");
539            default:
540                throw new BoxAPIException("Representation had unknown status");
541        }
542
543    }
544
545    private String pollRepInfo(URL infoURL) {
546
547        BoxJSONRequest infoRequest = new BoxJSONRequest(this.getAPI(), infoURL, HttpMethod.GET);
548        try (BoxJSONResponse infoResponse = infoRequest.send()) {
549            JsonObject response = infoResponse.getJsonObject();
550
551            Representation rep = new Representation(response);
552
553            String repState = rep.getStatus().getState();
554
555            switch (repState) {
556                case "viewable":
557                case "success":
558                    return rep.getContent().getUrlTemplate();
559                case "pending":
560                case "none":
561                    return null;
562                case "error":
563                    throw new BoxAPIException("Representation had error status");
564                default:
565                    throw new BoxAPIException("Representation had unknown status");
566            }
567        }
568    }
569
570    private void makeRepresentationContentRequest(
571        String representationURLTemplate, String assetPath, OutputStream output
572    ) {
573        try {
574            URL repURL = new URL(representationURLTemplate.replace("{+asset_path}", assetPath));
575            BoxAPIRequest repContentReq = new BoxAPIRequest(this.getAPI(), repURL, HttpMethod.GET);
576            BoxAPIResponse response = repContentReq.send();
577            writeStream(response, output);
578        } catch (MalformedURLException ex) {
579
580            throw new BoxAPIException("Could not generate representation content URL");
581        }
582    }
583
584    /**
585     * Updates the information about this file with any info fields that have been modified locally.
586     *
587     * <p>The only fields that will be updated are the ones that have been modified locally. For example, the following
588     * code won't update any information (or even send a network request) since none of the info's fields were
589     * changed:</p>
590     *
591     * <pre>BoxFile file = new File(api, id);
592     * BoxFile.Info info = file.getInfo();
593     * file.updateInfo(info);</pre>
594     *
595     * @param info the updated info.
596     */
597    public void updateInfo(BoxFile.Info info) {
598        URL url = FILE_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
599        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "PUT");
600        request.setBody(info.getPendingChanges());
601        try (BoxJSONResponse response = request.send()) {
602            JsonObject jsonObject = Json.parse(response.getJSON()).asObject();
603            info.update(jsonObject);
604        }
605    }
606
607    /**
608     * Gets up to 1000 versions of this file. Note that only users with premium accounts will be able to retrieve
609     * previous versions of their files. `fields` parameter is optional, if specified only requested fields will
610     * be returned:
611     * <pre>
612     * {@code
613     * new BoxFile(api, file_id).getVersions()       // will return all default fields
614     * new BoxFile(api, file_id).getVersions("name") // will return only specified fields
615     * }
616     * </pre>
617     *
618     * @param fields the fields to retrieve. If nothing provided default fields will be returned.
619     *               You can find list of available fields at {@link BoxFile#ALL_VERSION_FIELDS}
620     * @return a list of previous file versions.
621     */
622    public Collection<BoxFileVersion> getVersions(String... fields) {
623        return getVersionsRange(0, BoxFileVersion.DEFAULT_LIMIT, fields);
624    }
625
626
627    /**
628     * Retrieves a specific range of versions of this file.
629     *
630     * @param offset the index of the first version of this file to retrieve.
631     * @param limit  the maximum number of versions to retrieve after the offset.
632     * @param fields the fields to retrieve.
633     * @return a partial collection containing the specified range of versions of this file.
634     */
635    public PartialCollection<BoxFileVersion> getVersionsRange(long offset, long limit, String... fields) {
636        QueryStringBuilder builder = new QueryStringBuilder()
637                .appendParam("limit", limit)
638                .appendParam("offset", offset);
639
640        if (fields.length > 0) {
641            builder.appendParam("fields", fields);
642        }
643
644        URL url = VERSIONS_URL_TEMPLATE.buildWithQuery(this.getAPI().getBaseURL(), builder.toString(), this.getID());
645        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "GET");
646        try (BoxJSONResponse response = request.send()) {
647
648            JsonObject jsonObject = Json.parse(response.getJSON()).asObject();
649            String totalCountString = jsonObject.get("total_count").toString();
650            long fullSize = Double.valueOf(totalCountString).longValue();
651            PartialCollection<BoxFileVersion> versions = new PartialCollection<>(offset, limit, fullSize);
652            JsonArray entries = jsonObject.get("entries").asArray();
653            for (JsonValue entry : entries) {
654                versions.add(new BoxFileVersion(this.getAPI(), entry.asObject(), this.getID()));
655            }
656
657            return versions;
658        }
659    }
660
661    /**
662     * Checks if a new version of the file can be uploaded with the specified name.
663     *
664     * @param name the new name for the file.
665     * @return whether or not the file version can be uploaded.
666     */
667    public boolean canUploadVersion(String name) {
668        return this.canUploadVersion(name, 0);
669    }
670
671    /**
672     * Checks if a new version of the file can be uploaded with the specified name and size.
673     *
674     * @param name     the new name for the file.
675     * @param fileSize the size of the new version content in bytes.
676     * @return whether the file version can be uploaded.
677     */
678    public boolean canUploadVersion(String name, long fileSize) {
679
680        URL url = getDownloadUrl();
681        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "OPTIONS");
682
683        JsonObject preflightInfo = new JsonObject();
684        if (name != null) {
685            preflightInfo.add("name", name);
686        }
687
688        preflightInfo.add("size", fileSize);
689
690        request.setBody(preflightInfo.toString());
691        try (BoxAPIResponse response = request.send()) {
692            return response.getResponseCode() == 200;
693        } catch (BoxAPIException ex) {
694            if (ex.getResponseCode() >= 400 && ex.getResponseCode() < 500) {
695                // This looks like an error response, meaning the upload would fail
696                return false;
697            } else {
698                // This looks like a network error or server error, rethrow exception
699                throw ex;
700            }
701        }
702    }
703
704    /**
705     * Uploads a new version of this file, replacing the current version. Note that only users with premium accounts
706     * will be able to view and recover previous versions of the file.
707     *
708     * @param fileContent a stream containing the new file contents.
709     * @return the uploaded file version.
710     */
711    public BoxFile.Info uploadNewVersion(InputStream fileContent) {
712        return this.uploadNewVersion(fileContent, null);
713    }
714
715    /**
716     * Uploads a new version of this file, replacing the current version. Note that only users with premium accounts
717     * will be able to view and recover previous versions of the file.
718     *
719     * @param fileContent     a stream containing the new file contents.
720     * @param fileContentSHA1 a string containing the SHA1 hash of the new file contents.
721     * @return the uploaded file version.
722     */
723    public BoxFile.Info uploadNewVersion(InputStream fileContent, String fileContentSHA1) {
724        return this.uploadNewVersion(fileContent, fileContentSHA1, null);
725    }
726
727    /**
728     * Uploads a new version of this file, replacing the current version. Note that only users with premium accounts
729     * will be able to view and recover previous versions of the file.
730     *
731     * @param fileContent     a stream containing the new file contents.
732     * @param fileContentSHA1 a string containing the SHA1 hash of the new file contents.
733     * @param modified        the date that the new version was modified.
734     * @return the uploaded file version.
735     */
736    public BoxFile.Info uploadNewVersion(InputStream fileContent, String fileContentSHA1, Date modified) {
737        return this.uploadNewVersion(fileContent, fileContentSHA1, modified, 0, null);
738    }
739
740    /**
741     * Uploads a new version of this file, replacing the current version. Note that only users with premium accounts
742     * will be able to view and recover previous versions of the file.
743     *
744     * @param fileContent     a stream containing the new file contents.
745     * @param fileContentSHA1 a string containing the SHA1 hash of the new file contents.
746     * @param modified        the date that the new version was modified.
747     * @param name            the new name for the file
748     * @return the uploaded file version.
749     */
750    public BoxFile.Info uploadNewVersion(InputStream fileContent, String fileContentSHA1, Date modified, String name) {
751        return this.uploadNewVersion(fileContent, fileContentSHA1, modified, name, 0, null);
752    }
753
754    /**
755     * Uploads a new version of this file, replacing the current version, while reporting the progress to a
756     * ProgressListener. Note that only users with premium accounts will be able to view and recover previous versions
757     * of the file.
758     *
759     * @param fileContent a stream containing the new file contents.
760     * @param modified    the date that the new version was modified.
761     * @param fileSize    the size of the file used for determining the progress of the upload.
762     * @param listener    a listener for monitoring the upload's progress.
763     * @return the uploaded file version.
764     */
765    public BoxFile.Info uploadNewVersion(InputStream fileContent, Date modified, long fileSize,
766                                         ProgressListener listener) {
767        return this.uploadNewVersion(fileContent, null, modified, fileSize, listener);
768    }
769
770    /**
771     * Uploads a new version of this file, replacing the current version, while reporting the progress to a
772     * ProgressListener. Note that only users with premium accounts will be able to view and recover previous versions
773     * of the file.
774     *
775     * @param fileContent     a stream containing the new file contents.
776     * @param fileContentSHA1 the SHA1 hash of the file contents. will be sent along in the Content-MD5 header
777     * @param modified        the date that the new version was modified.
778     * @param fileSize        the size of the file used for determining the progress of the upload.
779     * @param listener        a listener for monitoring the upload's progress.
780     * @return the uploaded file version.
781     */
782    public BoxFile.Info uploadNewVersion(InputStream fileContent, String fileContentSHA1, Date modified, long fileSize,
783                                         ProgressListener listener) {
784        return this.uploadNewVersion(fileContent, fileContentSHA1, modified, null, fileSize, listener);
785    }
786
787    /**
788     * Uploads a new version of this file, replacing the current version, while reporting the progress to a
789     * ProgressListener. Note that only users with premium accounts will be able to view and recover previous versions
790     * of the file.
791     *
792     * @param fileContent     a stream containing the new file contents.
793     * @param fileContentSHA1 the SHA1 hash of the file contents. will be sent along in the Content-MD5 header
794     * @param modified        the date that the new version was modified.
795     * @param name            the new name for the file
796     * @param fileSize        the size of the file used for determining the progress of the upload.
797     * @param listener        a listener for monitoring the upload's progress.
798     * @return the uploaded file version.
799     */
800    public BoxFile.Info uploadNewVersion(InputStream fileContent, String fileContentSHA1, Date modified, String name,
801                                         long fileSize, ProgressListener listener) {
802        URL uploadURL = CONTENT_URL_TEMPLATE.build(this.getAPI().getBaseUploadURL(), this.getID());
803        BoxMultipartRequest request = new BoxMultipartRequest(getAPI(), uploadURL);
804
805        if (fileSize > 0) {
806            request.setFile(fileContent, "", fileSize);
807        } else {
808            request.setFile(fileContent, "");
809        }
810
811        if (fileContentSHA1 != null) {
812            request.setContentSHA1(fileContentSHA1);
813        }
814
815        JsonObject attributesJSON = new JsonObject();
816        if (modified != null) {
817            attributesJSON.add("content_modified_at", BoxDateFormat.format(modified));
818        }
819
820        if (name != null) {
821            attributesJSON.add("name", name);
822        }
823
824        request.putField("attributes", attributesJSON.toString());
825
826        BoxJSONResponse response = null;
827        try {
828            if (listener == null) {
829                // upload is multipart request but response is JSON
830                response = (BoxJSONResponse) request.send();
831            } else {
832                // upload is multipart request but response is JSON
833                response = (BoxJSONResponse) request.send(listener);
834            }
835
836            String fileJSON = response.getJsonObject().get("entries").asArray().get(0).toString();
837
838            return new BoxFile.Info(fileJSON);
839        } finally {
840            Optional.ofNullable(response).ifPresent(BoxAPIResponse::close);
841        }
842    }
843
844    /**
845     * Gets an expiring URL for creating an embedded preview session. The URL will expire after 60 seconds and the
846     * preview session will expire after 60 minutes.
847     *
848     * @return the expiring preview link
849     */
850    public URL getPreviewLink() {
851        BoxFile.Info info = this.getInfo("expiring_embed_link");
852
853        return info.getPreviewLink();
854    }
855
856    /**
857     * Gets a list of any comments on this file.
858     *
859     * @return a list of comments on this file.
860     */
861    public List<BoxComment.Info> getComments() {
862        URL url = GET_COMMENTS_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
863        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "GET");
864        try (BoxJSONResponse response = request.send()) {
865            JsonObject responseJSON = Json.parse(response.getJSON()).asObject();
866
867            int totalCount = responseJSON.get("total_count").asInt();
868            List<BoxComment.Info> comments = new ArrayList<>(totalCount);
869            JsonArray entries = responseJSON.get("entries").asArray();
870            for (JsonValue value : entries) {
871                JsonObject commentJSON = value.asObject();
872                BoxComment comment = new BoxComment(this.getAPI(), commentJSON.get("id").asString());
873                BoxComment.Info info = comment.new Info(commentJSON);
874                comments.add(info);
875            }
876
877            return comments;
878        }
879    }
880
881    /**
882     * Gets a list of any tasks on this file with requested fields.
883     *
884     * @param fields optional fields to retrieve for this task.
885     * @return a list of tasks on this file.
886     */
887    public List<BoxTask.Info> getTasks(String... fields) {
888        QueryStringBuilder builder = new QueryStringBuilder();
889        if (fields.length > 0) {
890            builder.appendParam("fields", fields);
891        }
892        URL url = GET_TASKS_URL_TEMPLATE.buildWithQuery(this.getAPI().getBaseURL(), builder.toString(), this.getID());
893        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "GET");
894        try (BoxJSONResponse response = request.send()) {
895            JsonObject responseJSON = Json.parse(response.getJSON()).asObject();
896
897            int totalCount = responseJSON.get("total_count").asInt();
898            List<BoxTask.Info> tasks = new ArrayList<>(totalCount);
899            JsonArray entries = responseJSON.get("entries").asArray();
900            for (JsonValue value : entries) {
901                JsonObject taskJSON = value.asObject();
902                BoxTask task = new BoxTask(this.getAPI(), taskJSON.get("id").asString());
903                BoxTask.Info info = task.new Info(taskJSON);
904                tasks.add(info);
905            }
906
907            return tasks;
908        }
909    }
910
911    /**
912     * Creates metadata on this file in the global properties template.
913     *
914     * @param metadata The new metadata values.
915     * @return the metadata returned from the server.
916     */
917    public Metadata createMetadata(Metadata metadata) {
918        return this.createMetadata(Metadata.DEFAULT_METADATA_TYPE, metadata);
919    }
920
921    /**
922     * Creates metadata on this file in the specified template type.
923     *
924     * @param typeName the metadata template type name.
925     * @param metadata the new metadata values.
926     * @return the metadata returned from the server.
927     */
928    public Metadata createMetadata(String typeName, Metadata metadata) {
929        String scope = Metadata.scopeBasedOnType(typeName);
930        return this.createMetadata(typeName, scope, metadata);
931    }
932
933    /**
934     * Creates metadata on this file in the specified template type.
935     *
936     * @param typeName the metadata template type name.
937     * @param scope    the metadata scope (global or enterprise).
938     * @param metadata the new metadata values.
939     * @return the metadata returned from the server.
940     */
941    public Metadata createMetadata(String typeName, String scope, Metadata metadata) {
942        URL url = METADATA_URL_TEMPLATE.buildAlpha(this.getAPI().getBaseURL(), this.getID(), scope, typeName);
943        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "POST");
944        request.setBody(metadata.toString());
945        try (BoxJSONResponse response = request.send()) {
946            return new Metadata(Json.parse(response.getJSON()).asObject());
947        }
948    }
949
950    /**
951     * Sets the provided metadata on the file. If metadata has already been created on this file,
952     * it overwrites metadata keys specified in the `metadata` param.
953     *
954     * @param templateName the name of the metadata template.
955     * @param scope        the scope of the template (usually "global" or "enterprise").
956     * @param metadata     the new metadata values.
957     * @return the metadata returned from the server.
958     */
959    public Metadata setMetadata(String templateName, String scope, Metadata metadata) {
960        try {
961            return this.createMetadata(templateName, scope, metadata);
962        } catch (BoxAPIException e) {
963            if (e.getResponseCode() == 409) {
964                if (metadata.getOperations().isEmpty()) {
965                    return getMetadata();
966                } else {
967                    return updateExistingTemplate(templateName, scope, metadata);
968                }
969            } else {
970                throw e;
971            }
972        }
973    }
974
975    private Metadata updateExistingTemplate(String templateName, String scope, Metadata metadata) {
976        Metadata metadataToUpdate = new Metadata(scope, templateName);
977        for (JsonValue value : metadata.getOperations()) {
978            if (value.asObject().get("value").isNumber()) {
979                metadataToUpdate.add(value.asObject().get("path").asString(),
980                    value.asObject().get("value").asDouble());
981            } else if (value.asObject().get("value").isString()) {
982                metadataToUpdate.add(value.asObject().get("path").asString(),
983                    value.asObject().get("value").asString());
984            } else if (value.asObject().get("value").isArray()) {
985                ArrayList<String> list = new ArrayList<>();
986                for (JsonValue jsonValue : value.asObject().get("value").asArray()) {
987                    list.add(jsonValue.asString());
988                }
989                metadataToUpdate.add(value.asObject().get("path").asString(), list);
990            }
991        }
992        return this.updateMetadata(metadataToUpdate);
993    }
994
995    /**
996     * Adds a metadata classification to the specified file.
997     *
998     * @param classificationType the metadata classification type.
999     * @return the metadata classification type added to the file.
1000     */
1001    public String addClassification(String classificationType) {
1002        Metadata metadata = new Metadata().add(Metadata.CLASSIFICATION_KEY, classificationType);
1003        Metadata classification = this.createMetadata(Metadata.CLASSIFICATION_TEMPLATE_KEY,
1004            "enterprise", metadata);
1005
1006        return classification.getString(Metadata.CLASSIFICATION_KEY);
1007    }
1008
1009    /**
1010     * Updates a metadata classification on the specified file.
1011     *
1012     * @param classificationType the metadata classification type.
1013     * @return the new metadata classification type updated on the file.
1014     */
1015    public String updateClassification(String classificationType) {
1016        Metadata metadata = new Metadata("enterprise", Metadata.CLASSIFICATION_TEMPLATE_KEY);
1017        metadata.add("/Box__Security__Classification__Key", classificationType);
1018        Metadata classification = this.updateMetadata(metadata);
1019
1020        return classification.getString(Metadata.CLASSIFICATION_KEY);
1021    }
1022
1023    /**
1024     * Attempts to add classification to a file. If classification already exists then do update.
1025     *
1026     * @param classificationType the metadata classification type.
1027     * @return the metadata classification type on the file.
1028     */
1029    public String setClassification(String classificationType) {
1030        Metadata metadata = new Metadata().add(Metadata.CLASSIFICATION_KEY, classificationType);
1031        Metadata classification;
1032
1033        try {
1034            classification = this.createMetadata(Metadata.CLASSIFICATION_TEMPLATE_KEY, "enterprise", metadata);
1035        } catch (BoxAPIException e) {
1036            if (e.getResponseCode() == 409) {
1037                metadata = new Metadata("enterprise", Metadata.CLASSIFICATION_TEMPLATE_KEY);
1038                metadata.replace(Metadata.CLASSIFICATION_KEY, classificationType);
1039                classification = this.updateMetadata(metadata);
1040            } else {
1041                throw e;
1042            }
1043        }
1044
1045        return classification.getString(Metadata.CLASSIFICATION_KEY);
1046    }
1047
1048    /**
1049     * Gets the classification type for the specified file.
1050     *
1051     * @return the metadata classification type on the file.
1052     */
1053    public String getClassification() {
1054        Metadata metadata;
1055        try {
1056            metadata = this.getMetadata(Metadata.CLASSIFICATION_TEMPLATE_KEY);
1057
1058        } catch (BoxAPIException e) {
1059            JsonObject responseObject = Json.parse(e.getResponse()).asObject();
1060            String code = responseObject.get("code").asString();
1061
1062            if (e.getResponseCode() == 404 && code.equals("instance_not_found")) {
1063                return null;
1064            } else {
1065                throw e;
1066            }
1067        }
1068
1069        return metadata.getString(Metadata.CLASSIFICATION_KEY);
1070    }
1071
1072    /**
1073     * Deletes the classification on the file.
1074     */
1075    public void deleteClassification() {
1076        this.deleteMetadata(Metadata.CLASSIFICATION_TEMPLATE_KEY, "enterprise");
1077    }
1078
1079    /**
1080     * Locks a file.
1081     *
1082     * @return the lock returned from the server.
1083     */
1084    public BoxLock lock() {
1085        return this.lock(null, false);
1086    }
1087
1088    /**
1089     * Locks a file.
1090     *
1091     * @param isDownloadPrevented is downloading of file prevented when locked.
1092     * @return the lock returned from the server.
1093     */
1094    public BoxLock lock(boolean isDownloadPrevented) {
1095        return this.lock(null, isDownloadPrevented);
1096    }
1097
1098    /**
1099     * Locks a file.
1100     *
1101     * @param expiresAt expiration date of the lock.
1102     * @return the lock returned from the server.
1103     */
1104    public BoxLock lock(Date expiresAt) {
1105        return this.lock(expiresAt, false);
1106    }
1107
1108    /**
1109     * Locks a file.
1110     *
1111     * @param expiresAt           expiration date of the lock.
1112     * @param isDownloadPrevented is downloading of file prevented when locked.
1113     * @return the lock returned from the server.
1114     */
1115    public BoxLock lock(Date expiresAt, boolean isDownloadPrevented) {
1116        String queryString = new QueryStringBuilder().appendParam("fields", "lock").toString();
1117        URL url = FILE_URL_TEMPLATE.buildWithQuery(this.getAPI().getBaseURL(), queryString, this.getID());
1118        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "PUT");
1119
1120        JsonObject lockConfig = new JsonObject();
1121        lockConfig.add("type", "lock");
1122        if (expiresAt != null) {
1123            lockConfig.add("expires_at", BoxDateFormat.format(expiresAt));
1124        }
1125        lockConfig.add("is_download_prevented", isDownloadPrevented);
1126
1127        JsonObject requestJSON = new JsonObject();
1128        requestJSON.add("lock", lockConfig);
1129        request.setBody(requestJSON.toString());
1130
1131        try (BoxJSONResponse response = request.send()) {
1132
1133            JsonObject responseJSON = Json.parse(response.getJSON()).asObject();
1134            JsonValue lockValue = responseJSON.get("lock");
1135            JsonObject lockJSON = Json.parse(lockValue.toString()).asObject();
1136
1137            return new BoxLock(lockJSON, this.getAPI());
1138        }
1139    }
1140
1141    /**
1142     * Unlocks a file.
1143     */
1144    public void unlock() {
1145        String queryString = new QueryStringBuilder().appendParam("fields", "lock").toString();
1146        URL url = FILE_URL_TEMPLATE.buildWithQuery(this.getAPI().getBaseURL(), queryString, this.getID());
1147        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "PUT");
1148
1149        JsonObject lockObject = new JsonObject();
1150        lockObject.add("lock", NULL);
1151
1152        request.setBody(lockObject.toString());
1153        request.send().close();
1154    }
1155
1156    /**
1157     * Used to retrieve all metadata associated with the file.
1158     *
1159     * @param fields the optional fields to retrieve.
1160     * @return An iterable of metadata instances associated with the file.
1161     */
1162    public Iterable<Metadata> getAllMetadata(String... fields) {
1163        return Metadata.getAllMetadata(this, fields);
1164    }
1165
1166    /**
1167     * Gets the file properties metadata.
1168     *
1169     * @return the metadata returned from the server.
1170     */
1171    public Metadata getMetadata() {
1172        return this.getMetadata(Metadata.DEFAULT_METADATA_TYPE);
1173    }
1174
1175    /**
1176     * Gets the file metadata of specified template type.
1177     *
1178     * @param typeName the metadata template type name.
1179     * @return the metadata returned from the server.
1180     */
1181    public Metadata getMetadata(String typeName) {
1182        String scope = Metadata.scopeBasedOnType(typeName);
1183        return this.getMetadata(typeName, scope);
1184    }
1185
1186    /**
1187     * Gets the file metadata of specified template type.
1188     *
1189     * @param typeName the metadata template type name.
1190     * @param scope    the metadata scope (global or enterprise).
1191     * @return the metadata returned from the server.
1192     */
1193    public Metadata getMetadata(String typeName, String scope) {
1194        URL url = METADATA_URL_TEMPLATE.buildAlpha(this.getAPI().getBaseURL(), this.getID(), scope, typeName);
1195        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "GET");
1196        try (BoxJSONResponse response = request.send()) {
1197            return new Metadata(Json.parse(response.getJSON()).asObject());
1198        }
1199    }
1200
1201    /**
1202     * Updates the file metadata.
1203     *
1204     * @param metadata the new metadata values.
1205     * @return the metadata returned from the server.
1206     */
1207    public Metadata updateMetadata(Metadata metadata) {
1208        String scope;
1209        if (metadata.getScope().equals(Metadata.GLOBAL_METADATA_SCOPE)) {
1210            scope = Metadata.GLOBAL_METADATA_SCOPE;
1211        } else if (metadata.getScope().startsWith(Metadata.ENTERPRISE_METADATA_SCOPE)) {
1212            scope = metadata.getScope();
1213        } else {
1214            scope = Metadata.ENTERPRISE_METADATA_SCOPE;
1215        }
1216
1217        URL url = METADATA_URL_TEMPLATE.buildAlpha(this.getAPI().getBaseURL(), this.getID(),
1218            scope, metadata.getTemplateName());
1219        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "PUT", APPLICATION_JSON_PATCH);
1220        request.setBody(metadata.getPatch());
1221        try (BoxJSONResponse response = request.send()) {
1222            return new Metadata(Json.parse(response.getJSON()).asObject());
1223        }
1224    }
1225
1226    /**
1227     * Deletes the file properties metadata.
1228     */
1229    public void deleteMetadata() {
1230        this.deleteMetadata(Metadata.DEFAULT_METADATA_TYPE);
1231    }
1232
1233    /**
1234     * Deletes the file metadata of specified template type.
1235     *
1236     * @param typeName the metadata template type name.
1237     */
1238    public void deleteMetadata(String typeName) {
1239        String scope = Metadata.scopeBasedOnType(typeName);
1240        this.deleteMetadata(typeName, scope);
1241    }
1242
1243    /**
1244     * Deletes the file metadata of specified template type.
1245     *
1246     * @param typeName the metadata template type name.
1247     * @param scope    the metadata scope (global or enterprise).
1248     */
1249    public void deleteMetadata(String typeName, String scope) {
1250        URL url = METADATA_URL_TEMPLATE.buildAlpha(this.getAPI().getBaseURL(), this.getID(), scope, typeName);
1251        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "DELETE");
1252        request.send().close();
1253    }
1254
1255    /**
1256     * Used to retrieve the watermark for the file.
1257     * If the file does not have a watermark applied to it, a 404 Not Found will be returned by API.
1258     *
1259     * @param fields the fields to retrieve.
1260     * @return the watermark associated with the file.
1261     */
1262    public BoxWatermark getWatermark(String... fields) {
1263        return this.getWatermark(FILE_URL_TEMPLATE, fields);
1264    }
1265
1266    /**
1267     * Used to apply or update the watermark for the file.
1268     *
1269     * @return the watermark associated with the file.
1270     */
1271    public BoxWatermark applyWatermark() {
1272        return this.applyWatermark(FILE_URL_TEMPLATE, BoxWatermark.WATERMARK_DEFAULT_IMPRINT);
1273    }
1274
1275    /**
1276     * Removes a watermark from the file.
1277     * If the file did not have a watermark applied to it, a 404 Not Found will be returned by API.
1278     */
1279    public void removeWatermark() {
1280        this.removeWatermark(FILE_URL_TEMPLATE);
1281    }
1282
1283    /**
1284     * {@inheritDoc}
1285     */
1286    @Override
1287    public BoxFile.Info setCollections(BoxCollection... collections) {
1288        JsonArray jsonArray = new JsonArray();
1289        for (BoxCollection collection : collections) {
1290            JsonObject collectionJSON = new JsonObject();
1291            collectionJSON.add("id", collection.getID());
1292            jsonArray.add(collectionJSON);
1293        }
1294        JsonObject infoJSON = new JsonObject();
1295        infoJSON.add("collections", jsonArray);
1296
1297        String queryString = new QueryStringBuilder().appendParam("fields", ALL_FIELDS).toString();
1298        URL url = FILE_URL_TEMPLATE.buildWithQuery(this.getAPI().getBaseURL(), queryString, this.getID());
1299        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "PUT");
1300        request.setBody(infoJSON.toString());
1301        try (BoxJSONResponse response = request.send()) {
1302            JsonObject jsonObject = Json.parse(response.getJSON()).asObject();
1303            return new Info(jsonObject);
1304        }
1305    }
1306
1307    /**
1308     * Creates an upload session to create a new version of a file in chunks.
1309     * This will first verify that the version can be created and then open a session for uploading pieces of the file.
1310     *
1311     * @param fileSize the size of the file that will be uploaded.
1312     * @return the created upload session instance.
1313     */
1314    public BoxFileUploadSession.Info createUploadSession(long fileSize) {
1315        URL url = UPLOAD_SESSION_URL_TEMPLATE.build(this.getAPI().getBaseUploadURL(), this.getID());
1316
1317        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "POST");
1318        request.addHeader("Content-Type", APPLICATION_JSON);
1319
1320        JsonObject body = new JsonObject();
1321        body.add("file_size", fileSize);
1322        request.setBody(body.toString());
1323
1324        try (BoxJSONResponse response = request.send()) {
1325            JsonObject jsonObject = Json.parse(response.getJSON()).asObject();
1326
1327            String sessionId = jsonObject.get("id").asString();
1328            BoxFileUploadSession session = new BoxFileUploadSession(this.getAPI(), sessionId);
1329            return session.new Info(jsonObject);
1330        }
1331    }
1332
1333    /**
1334     * Creates a new version of a file.
1335     *
1336     * @param inputStream the stream instance that contains the data.
1337     * @param fileSize    the size of the file that will be uploaded.
1338     * @return the created file instance.
1339     * @throws InterruptedException when a thread execution is interrupted.
1340     * @throws IOException          when reading a stream throws exception.
1341     */
1342    public BoxFile.Info uploadLargeFile(InputStream inputStream, long fileSize)
1343        throws InterruptedException, IOException {
1344        URL url = UPLOAD_SESSION_URL_TEMPLATE.build(this.getAPI().getBaseUploadURL(), this.getID());
1345        return new LargeFileUpload().upload(this.getAPI(), inputStream, url, fileSize);
1346    }
1347
1348    /**
1349     * Creates a new version of a file.  Also sets file attributes.
1350     *
1351     * @param inputStream    the stream instance that contains the data.
1352     * @param fileSize       the size of the file that will be uploaded.
1353     * @param fileAttributes file attributes to set
1354     * @return the created file instance.
1355     * @throws InterruptedException when a thread execution is interrupted.
1356     * @throws IOException          when reading a stream throws exception.
1357     */
1358    public BoxFile.Info uploadLargeFile(InputStream inputStream, long fileSize, Map<String, String> fileAttributes)
1359        throws InterruptedException, IOException {
1360        URL url = UPLOAD_SESSION_URL_TEMPLATE.build(this.getAPI().getBaseUploadURL(), this.getID());
1361        return new LargeFileUpload().upload(this.getAPI(), inputStream, url, fileSize, fileAttributes);
1362    }
1363
1364    /**
1365     * Creates a new version of a file using specified number of parallel http connections.
1366     *
1367     * @param inputStream          the stream instance that contains the data.
1368     * @param fileSize             the size of the file that will be uploaded.
1369     * @param nParallelConnections number of parallel http connections to use
1370     * @param timeOut              time to wait before killing the job
1371     * @param unit                 time unit for the time wait value
1372     * @return the created file instance.
1373     * @throws InterruptedException when a thread execution is interrupted.
1374     * @throws IOException          when reading a stream throws exception.
1375     */
1376    public BoxFile.Info uploadLargeFile(InputStream inputStream, long fileSize,
1377                                        int nParallelConnections, long timeOut, TimeUnit unit)
1378        throws InterruptedException, IOException {
1379        URL url = UPLOAD_SESSION_URL_TEMPLATE.build(this.getAPI().getBaseUploadURL(), this.getID());
1380        return new LargeFileUpload(nParallelConnections, timeOut, unit)
1381            .upload(this.getAPI(), inputStream, url, fileSize);
1382    }
1383
1384    /**
1385     * Creates a new version of a file using specified number of parallel http connections.  Also sets file attributes.
1386     *
1387     * @param inputStream          the stream instance that contains the data.
1388     * @param fileSize             the size of the file that will be uploaded.
1389     * @param nParallelConnections number of parallel http connections to use
1390     * @param timeOut              time to wait before killing the job
1391     * @param unit                 time unit for the time wait value
1392     * @param fileAttributes       file attributes to set
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                                        int nParallelConnections, long timeOut, TimeUnit unit,
1399                                        Map<String, String> fileAttributes)
1400        throws InterruptedException, IOException {
1401        URL url = UPLOAD_SESSION_URL_TEMPLATE.build(this.getAPI().getBaseUploadURL(), this.getID());
1402        return new LargeFileUpload(nParallelConnections, timeOut, unit)
1403            .upload(this.getAPI(), inputStream, url, fileSize, fileAttributes);
1404    }
1405
1406    private BoxCollaboration.Info collaborate(JsonObject accessibleByField, BoxCollaboration.Role role,
1407                                              Boolean notify, Boolean canViewPath, Date expiresAt,
1408                                              Boolean isAccessOnly) {
1409
1410        JsonObject itemField = new JsonObject();
1411        itemField.add("id", this.getID());
1412        itemField.add("type", "file");
1413
1414        return BoxCollaboration.create(this.getAPI(), accessibleByField, itemField, role, notify, canViewPath,
1415                expiresAt, isAccessOnly);
1416    }
1417
1418    /**
1419     * Adds a collaborator to this file.
1420     *
1421     * @param collaborator the collaborator to add.
1422     * @param role         the role of the collaborator.
1423     * @param notify       determines if the user (or all the users in the group) will receive email notifications.
1424     * @param canViewPath  whether view path collaboration feature is enabled or not.
1425     * @param expiresAt    when the collaboration should expire.
1426     * @param isAccessOnly whether the collaboration is access only or not.
1427     * @return info about the new collaboration.
1428     */
1429    public BoxCollaboration.Info collaborate(BoxCollaborator collaborator, BoxCollaboration.Role role,
1430                                             Boolean notify, Boolean canViewPath,
1431                                             Date expiresAt, Boolean isAccessOnly) {
1432        JsonObject accessibleByField = new JsonObject();
1433        accessibleByField.add("id", collaborator.getID());
1434
1435        if (collaborator instanceof BoxUser) {
1436            accessibleByField.add("type", "user");
1437        } else if (collaborator instanceof BoxGroup) {
1438            accessibleByField.add("type", "group");
1439        } else {
1440            throw new IllegalArgumentException("The given collaborator is of an unknown type.");
1441        }
1442        return this.collaborate(accessibleByField, role, notify, canViewPath, 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     * @return info about the new collaboration.
1453     */
1454    public BoxCollaboration.Info collaborate(BoxCollaborator collaborator, BoxCollaboration.Role role,
1455                                             Boolean notify, Boolean canViewPath) {
1456        return this.collaborate(collaborator, role, notify, canViewPath, null, null);
1457    }
1458
1459    /**
1460     * Adds a collaborator to this folder. An email will be sent to the collaborator if they don't already have a Box
1461     * account.
1462     *
1463     * @param email       the email address of the collaborator to add.
1464     * @param role        the role of the collaborator.
1465     * @param notify      determines if the user (or all the users in the group) will receive email notifications.
1466     * @param canViewPath whether view path collaboration feature is enabled or not.
1467     * @param expiresAt    when the collaboration should expire.
1468     * @param isAccessOnly whether the collaboration is access only or not.
1469     * @return info about the new collaboration.
1470     */
1471    public BoxCollaboration.Info collaborate(String email, BoxCollaboration.Role role,
1472                                             Boolean notify, Boolean canViewPath,
1473                                             Date expiresAt, Boolean isAccessOnly) {
1474        JsonObject accessibleByField = new JsonObject();
1475        accessibleByField.add("login", email);
1476        accessibleByField.add("type", "user");
1477
1478        return this.collaborate(accessibleByField, role, notify, canViewPath, expiresAt, isAccessOnly);
1479    }
1480
1481    /**
1482     * Adds a collaborator to this folder. An email will be sent to the collaborator if they don't already have a Box
1483     * account.
1484     *
1485     * @param email       the email address of the collaborator to add.
1486     * @param role        the role of the collaborator.
1487     * @param notify      determines if the user (or all the users in the group) will receive email notifications.
1488     * @param canViewPath whether view path collaboration feature is enabled or not.
1489     * @return info about the new collaboration.
1490     */
1491    public BoxCollaboration.Info collaborate(String email, BoxCollaboration.Role role,
1492                                             Boolean notify, Boolean canViewPath) {
1493        return this.collaborate(email, role, notify, canViewPath, null, null);
1494    }
1495
1496    /**
1497     * Used to retrieve all collaborations associated with the item.
1498     *
1499     * @param fields the optional fields to retrieve.
1500     * @return An iterable of metadata instances associated with the item.
1501     */
1502    public BoxResourceIterable<BoxCollaboration.Info> getAllFileCollaborations(String... fields) {
1503        return BoxCollaboration.getAllFileCollaborations(this.getAPI(), this.getID(),
1504            GET_COLLABORATORS_PAGE_SIZE, fields);
1505
1506    }
1507
1508    /**
1509     * Used to specify what filetype to request for a file thumbnail.
1510     */
1511    public enum ThumbnailFileType {
1512        /**
1513         * PNG image format.
1514         */
1515        PNG,
1516
1517        /**
1518         * JPG image format.
1519         */
1520        JPG
1521    }
1522
1523    /**
1524     * Enumerates the possible permissions that a user can have on a file.
1525     */
1526    public enum Permission {
1527        /**
1528         * The user can download the file.
1529         */
1530        CAN_DOWNLOAD("can_download"),
1531
1532        /**
1533         * The user can upload new versions of the file.
1534         */
1535        CAN_UPLOAD("can_upload"),
1536
1537        /**
1538         * The user can rename the file.
1539         */
1540        CAN_RENAME("can_rename"),
1541
1542        /**
1543         * The user can delete the file.
1544         */
1545        CAN_DELETE("can_delete"),
1546
1547        /**
1548         * The user can share the file.
1549         */
1550        CAN_SHARE("can_share"),
1551
1552        /**
1553         * The user can set the access level for shared links to the file.
1554         */
1555        CAN_SET_SHARE_ACCESS("can_set_share_access"),
1556
1557        /**
1558         * The user can preview the file.
1559         */
1560        CAN_PREVIEW("can_preview"),
1561
1562        /**
1563         * The user can comment on the file.
1564         */
1565        CAN_COMMENT("can_comment"),
1566
1567        /**
1568         * The user can place annotations on this file.
1569         */
1570        CAN_ANNOTATE("can_annotate"),
1571
1572        /**
1573         * The current user can invite new users to collaborate on this item, and the user can update the role of a
1574         * user already collaborated on this item.
1575         */
1576        CAN_INVITE_COLLABORATOR("can_invite_collaborator"),
1577
1578        /**
1579         * The user can view all annotations placed on this file.
1580         */
1581        CAN_VIEW_ANNOTATIONS_ALL("can_view_annotations_all"),
1582
1583        /**
1584         * The user can view annotations placed by themselves on this file.
1585         */
1586        CAN_VIEW_ANNOTATIONS_SELF("can_view_annotations_self");
1587
1588        private final String jsonValue;
1589
1590        Permission(String jsonValue) {
1591            this.jsonValue = jsonValue;
1592        }
1593
1594        static Permission fromJSONValue(String jsonValue) {
1595            return Permission.valueOf(jsonValue.toUpperCase());
1596        }
1597
1598        String toJSONValue() {
1599            return this.jsonValue;
1600        }
1601    }
1602
1603    /**
1604     * Contains information about a BoxFile.
1605     */
1606    public class Info extends BoxItem.Info {
1607        private String sha1;
1608        private String versionNumber;
1609        private long commentCount;
1610        private EnumSet<Permission> permissions;
1611        private String extension;
1612        private boolean isPackage;
1613        private BoxFileVersion version;
1614        private URL previewLink;
1615        private BoxLock lock;
1616        private boolean isWatermarked;
1617        private boolean isExternallyOwned;
1618        private Map<String, Map<String, Metadata>> metadataMap;
1619        private List<Representation> representations;
1620        private List<String> allowedInviteeRoles;
1621        private Boolean hasCollaborations;
1622        private String uploaderDisplayName;
1623        private BoxClassification classification;
1624        private Date dispositionAt;
1625        private boolean isAccessibleViaSharedLink;
1626
1627        /**
1628         * Constructs an empty Info object.
1629         */
1630        public Info() {
1631            super();
1632        }
1633
1634        /**
1635         * Constructs an Info object by parsing information from a JSON string.
1636         *
1637         * @param json the JSON string to parse.
1638         */
1639        public Info(String json) {
1640            super(json);
1641        }
1642
1643        /**
1644         * Constructs an Info object using an already parsed JSON object.
1645         *
1646         * @param jsonObject the parsed JSON object.
1647         */
1648        public Info(JsonObject jsonObject) {
1649            super(jsonObject);
1650        }
1651
1652        @Override
1653        public BoxFile getResource() {
1654            return BoxFile.this;
1655        }
1656
1657        /**
1658         * Gets the SHA1 hash of the file.
1659         *
1660         * @return the SHA1 hash of the file.
1661         */
1662        public String getSha1() {
1663            return this.sha1;
1664        }
1665
1666        /**
1667         * Gets the lock of the file.
1668         *
1669         * @return the lock of the file.
1670         */
1671        public BoxLock getLock() {
1672            return this.lock;
1673        }
1674
1675        /**
1676         * Gets the current version number of the file.
1677         *
1678         * @return the current version number of the file.
1679         */
1680        public String getVersionNumber() {
1681            return this.versionNumber;
1682        }
1683
1684        /**
1685         * Gets the number of comments on the file.
1686         *
1687         * @return the number of comments on the file.
1688         */
1689        public long getCommentCount() {
1690            return this.commentCount;
1691        }
1692
1693        /**
1694         * Gets the permissions that the current user has on the file.
1695         *
1696         * @return the permissions that the current user has on the file.
1697         */
1698        public EnumSet<Permission> getPermissions() {
1699            return this.permissions;
1700        }
1701
1702        /**
1703         * Gets the extension suffix of the file, excluding the dot.
1704         *
1705         * @return the extension of the file.
1706         */
1707        public String getExtension() {
1708            return this.extension;
1709        }
1710
1711        /**
1712         * Gets whether or not the file is an OSX package.
1713         *
1714         * @return true if the file is an OSX package; otherwise false.
1715         */
1716        public boolean getIsPackage() {
1717            return this.isPackage;
1718        }
1719
1720        /**
1721         * Gets the current version details of the file.
1722         *
1723         * @return the current version details of the file.
1724         */
1725        public BoxFileVersion getVersion() {
1726            return this.version;
1727        }
1728
1729        /**
1730         * Gets the current expiring preview link.
1731         *
1732         * @return the expiring preview link
1733         */
1734        public URL getPreviewLink() {
1735            return this.previewLink;
1736        }
1737
1738        /**
1739         * Gets flag indicating whether this file is Watermarked.
1740         *
1741         * @return whether the file is watermarked or not
1742         */
1743        public boolean getIsWatermarked() {
1744            return this.isWatermarked;
1745        }
1746
1747        /**
1748         * Returns the allowed invitee roles for this file item.
1749         *
1750         * @return the list of roles allowed for invited collaborators.
1751         */
1752        public List<String> getAllowedInviteeRoles() {
1753            return this.allowedInviteeRoles;
1754        }
1755
1756        /**
1757         * Returns the indicator for whether this file item has collaborations.
1758         *
1759         * @return indicator for whether this file item has collaborations.
1760         */
1761        public Boolean getHasCollaborations() {
1762            return this.hasCollaborations;
1763        }
1764
1765        /**
1766         * Gets the metadata on this file associated with a specified scope and template.
1767         * Makes an attempt to get metadata that was retrieved using getInfo(String ...) method.
1768         *
1769         * @param templateName the metadata template type name.
1770         * @param scope        the scope of the template (usually "global" or "enterprise").
1771         * @return the metadata returned from the server.
1772         */
1773        public Metadata getMetadata(String templateName, String scope) {
1774            try {
1775                return this.metadataMap.get(scope).get(templateName);
1776            } catch (NullPointerException e) {
1777                return null;
1778            }
1779        }
1780
1781        /**
1782         * Returns the field for indicating whether a file is owned by a user outside the enterprise.
1783         *
1784         * @return indicator for whether or not the file is owned by a user outside the enterprise.
1785         */
1786        public boolean getIsExternallyOwned() {
1787            return this.isExternallyOwned;
1788        }
1789
1790        /**
1791         * Get file's representations.
1792         *
1793         * @return list of representations
1794         */
1795        public List<Representation> getRepresentations() {
1796            return this.representations;
1797        }
1798
1799        /**
1800         * Returns user's name at the time of upload.
1801         *
1802         * @return user's name at the time of upload
1803         */
1804        public String getUploaderDisplayName() {
1805            return this.uploaderDisplayName;
1806        }
1807
1808        /**
1809         * Gets the metadata classification type of this file.
1810         *
1811         * @return the metadata classification type of this file.
1812         */
1813        public BoxClassification getClassification() {
1814            return this.classification;
1815        }
1816
1817        /**
1818         * Returns the retention expiration timestamp for the given file.
1819         *
1820         * @return Date representing expiration timestamp
1821         */
1822        public Date getDispositionAt() {
1823            return dispositionAt;
1824        }
1825
1826        /**
1827         * Modifies the retention expiration timestamp for the given file.
1828         * This date cannot be shortened once set on a file.
1829         *
1830         * @param dispositionAt Date representing expiration timestamp
1831         */
1832        public void setDispositionAt(Date dispositionAt) {
1833            this.dispositionAt = dispositionAt;
1834            this.addPendingChange("disposition_at", BoxDateFormat.format(dispositionAt));
1835        }
1836
1837        /**
1838         * Returns the flag indicating whether the file is accessible via a shared link.
1839         *
1840         * @return boolean flag indicating whether the file is accessible via a shared link.
1841         */
1842        public boolean getIsAccessibleViaSharedLink() {
1843            return this.isAccessibleViaSharedLink;
1844        }
1845
1846        @Override
1847        protected void parseJSONMember(JsonObject.Member member) {
1848            super.parseJSONMember(member);
1849
1850            String memberName = member.getName();
1851            JsonValue value = member.getValue();
1852            try {
1853                switch (memberName) {
1854                    case "sha1":
1855                        this.sha1 = value.asString();
1856                        break;
1857                    case "version_number":
1858                        this.versionNumber = value.asString();
1859                        break;
1860                    case "comment_count":
1861                        this.commentCount = value.asLong();
1862                        break;
1863                    case "permissions":
1864                        this.permissions = this.parsePermissions(value.asObject());
1865                        break;
1866                    case "extension":
1867                        this.extension = value.asString();
1868                        break;
1869                    case "is_package":
1870                        this.isPackage = value.asBoolean();
1871                        break;
1872                    case "has_collaborations":
1873                        this.hasCollaborations = value.asBoolean();
1874                        break;
1875                    case "is_externally_owned":
1876                        this.isExternallyOwned = value.asBoolean();
1877                        break;
1878                    case "file_version":
1879                        this.version = this.parseFileVersion(value.asObject());
1880                        break;
1881                    case "allowed_invitee_roles":
1882                        this.allowedInviteeRoles = this.parseAllowedInviteeRoles(value.asArray());
1883                        break;
1884                    case "expiring_embed_link":
1885                        try {
1886                            String urlString = member.getValue().asObject().get("url").asString();
1887                            this.previewLink = new URL(urlString);
1888                        } catch (MalformedURLException e) {
1889                            throw new BoxAPIException("Couldn't parse expiring_embed_link/url for file", e);
1890                        }
1891                        break;
1892                    case "lock":
1893                        if (value.isNull()) {
1894                            this.lock = null;
1895                        } else {
1896                            this.lock = new BoxLock(value.asObject(), BoxFile.this.getAPI());
1897                        }
1898                        break;
1899                    case "watermark_info":
1900                        this.isWatermarked = value.asObject().get("is_watermarked").asBoolean();
1901                        break;
1902                    case "metadata":
1903                        this.metadataMap = Parsers.parseAndPopulateMetadataMap(value.asObject());
1904                        break;
1905                    case "representations":
1906                        this.representations = Parsers.parseRepresentations(value.asObject());
1907                        break;
1908                    case "uploader_display_name":
1909                        this.uploaderDisplayName = value.asString();
1910                        break;
1911                    case "classification":
1912                        if (value.isNull()) {
1913                            this.classification = null;
1914                        } else {
1915                            this.classification = new BoxClassification(value.asObject());
1916                        }
1917                        break;
1918                    case "disposition_at":
1919                        this.dispositionAt = BoxDateFormat.parse(value.asString());
1920                        break;
1921                    case "is_accessible_via_shared_link":
1922                        this.isAccessibleViaSharedLink = value.asBoolean();
1923                        break;
1924                    default:
1925                        break;
1926                }
1927            } catch (Exception e) {
1928                throw new BoxDeserializationException(memberName, value.toString(), e);
1929            }
1930        }
1931
1932        @SuppressWarnings("checkstyle:MissingSwitchDefault")
1933        private EnumSet<Permission> parsePermissions(JsonObject jsonObject) {
1934            EnumSet<Permission> permissions = EnumSet.noneOf(Permission.class);
1935            for (JsonObject.Member member : jsonObject) {
1936                JsonValue value = member.getValue();
1937                if (value.isNull() || !value.asBoolean()) {
1938                    continue;
1939                }
1940
1941                switch (member.getName()) {
1942                    case "can_download":
1943                        permissions.add(Permission.CAN_DOWNLOAD);
1944                        break;
1945                    case "can_upload":
1946                        permissions.add(Permission.CAN_UPLOAD);
1947                        break;
1948                    case "can_rename":
1949                        permissions.add(Permission.CAN_RENAME);
1950                        break;
1951                    case "can_delete":
1952                        permissions.add(Permission.CAN_DELETE);
1953                        break;
1954                    case "can_share":
1955                        permissions.add(Permission.CAN_SHARE);
1956                        break;
1957                    case "can_set_share_access":
1958                        permissions.add(Permission.CAN_SET_SHARE_ACCESS);
1959                        break;
1960                    case "can_preview":
1961                        permissions.add(Permission.CAN_PREVIEW);
1962                        break;
1963                    case "can_comment":
1964                        permissions.add(Permission.CAN_COMMENT);
1965                        break;
1966                }
1967            }
1968
1969            return permissions;
1970        }
1971
1972        private BoxFileVersion parseFileVersion(JsonObject jsonObject) {
1973            return new BoxFileVersion(BoxFile.this.getAPI(), jsonObject, BoxFile.this.getID());
1974        }
1975
1976        private List<String> parseAllowedInviteeRoles(JsonArray jsonArray) {
1977            List<String> roles = new ArrayList<>(jsonArray.size());
1978            for (JsonValue value : jsonArray) {
1979                roles.add(value.asString());
1980            }
1981
1982            return roles;
1983        }
1984    }
1985
1986}