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