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