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