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