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