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