001package com.box.sdk;
002
003import static com.box.sdk.PagingParameters.DEFAULT_LIMIT;
004import static com.box.sdk.PagingParameters.marker;
005import static com.box.sdk.PagingParameters.offset;
006import static com.box.sdk.http.ContentType.APPLICATION_JSON_PATCH;
007
008import com.box.sdk.internal.utils.Parsers;
009import com.box.sdk.sharedlink.BoxSharedLinkRequest;
010import com.eclipsesource.json.Json;
011import com.eclipsesource.json.JsonArray;
012import com.eclipsesource.json.JsonObject;
013import com.eclipsesource.json.JsonValue;
014import java.io.IOException;
015import java.io.InputStream;
016import java.net.URL;
017import java.util.ArrayList;
018import java.util.Collection;
019import java.util.Date;
020import java.util.EnumSet;
021import java.util.Iterator;
022import java.util.List;
023import java.util.Map;
024import java.util.Optional;
025import java.util.concurrent.TimeUnit;
026
027/**
028 * Represents a folder on Box. This class can be used to iterate through a folder's contents,
029 * collaborate a folder with another user or group, and perform other common folder operations
030 * (move, copy, delete, etc.).
031 *
032 * <p>Unless otherwise noted, the methods in this class can throw an unchecked {@link
033 * BoxAPIException} (unchecked meaning that the compiler won't force you to handle it) if an error
034 * occurs. If you wish to implement custom error handling for errors related to the Box REST API,
035 * you should capture this exception explicitly.
036 */
037@BoxResourceType("folder")
038public class BoxFolder extends BoxItem implements Iterable<BoxItem.Info> {
039  /**
040   * An array of all possible folder fields that can be requested when calling {@link
041   * #getInfo(String...)}.
042   */
043  public static final String[] ALL_FIELDS = {
044    "type",
045    "id",
046    "sequence_id",
047    "etag",
048    "name",
049    "created_at",
050    "modified_at",
051    "description",
052    "size",
053    "path_collection",
054    "created_by",
055    "modified_by",
056    "trashed_at",
057    "purged_at",
058    "content_created_at",
059    "content_modified_at",
060    "owned_by",
061    "shared_link",
062    "folder_upload_email",
063    "parent",
064    "item_status",
065    "item_collection",
066    "sync_state",
067    "has_collaborations",
068    "permissions",
069    "tags",
070    "can_non_owners_invite",
071    "collections",
072    "watermark_info",
073    "metadata",
074    "is_externally_owned",
075    "is_collaboration_restricted_to_enterprise",
076    "allowed_shared_link_access_levels",
077    "allowed_invitee_roles",
078    "is_accessible_via_shared_link",
079    "can_non_owners_view_collaborators"
080  };
081  /** Create Folder URL Template. */
082  public static final URLTemplate CREATE_FOLDER_URL = new URLTemplate("folders");
083  /** Create Web Link URL Template. */
084  public static final URLTemplate CREATE_WEB_LINK_URL = new URLTemplate("web_links");
085  /** Copy Folder URL Template. */
086  public static final URLTemplate COPY_FOLDER_URL = new URLTemplate("folders/%s/copy");
087  /** Delete Folder URL Template. */
088  public static final URLTemplate DELETE_FOLDER_URL = new URLTemplate("folders/%s?recursive=%b");
089  /** Folder Info URL Template. */
090  public static final URLTemplate FOLDER_INFO_URL_TEMPLATE = new URLTemplate("folders/%s");
091  /** Upload File URL Template. */
092  public static final URLTemplate UPLOAD_FILE_URL = new URLTemplate("files/content");
093  /** Add Collaboration URL Template. */
094  public static final URLTemplate ADD_COLLABORATION_URL = new URLTemplate("collaborations");
095  /** Get Collaborations URL Template. */
096  public static final URLTemplate GET_COLLABORATIONS_URL =
097      new URLTemplate("folders/%s/collaborations");
098  /** Get Items URL Template. */
099  public static final URLTemplate GET_ITEMS_URL = new URLTemplate("folders/%s/items/");
100  /** Search URL Template. */
101  public static final URLTemplate SEARCH_URL_TEMPLATE = new URLTemplate("search");
102  /** Metadata URL Template. */
103  public static final URLTemplate METADATA_URL_TEMPLATE =
104      new URLTemplate("folders/%s/metadata/%s/%s");
105  /** Upload Session URL Template. */
106  public static final URLTemplate UPLOAD_SESSION_URL_TEMPLATE =
107      new URLTemplate("files/upload_sessions");
108  /** Folder Locks URL Template. */
109  public static final URLTemplate FOLDER_LOCK_URL_TEMPLATE = new URLTemplate("folder_locks");
110  /** Describes folder item type. */
111  static final String TYPE = "folder";
112
113  /**
114   * Constructs a BoxFolder for a folder with a given ID.
115   *
116   * @param api the API connection to be used by the folder.
117   * @param id the ID of the folder.
118   */
119  public BoxFolder(BoxAPIConnection api, String id) {
120    super(api, id);
121  }
122
123  /**
124   * Gets the current user's root folder.
125   *
126   * @param api the API connection to be used by the folder.
127   * @return the user's root folder.
128   */
129  public static BoxFolder getRootFolder(BoxAPIConnection api) {
130    return new BoxFolder(api, "0");
131  }
132
133  /** {@inheritDoc} */
134  @Override
135  protected URL getItemURL() {
136    return FOLDER_INFO_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
137  }
138
139  /**
140   * Adds a collaborator to this folder.
141   *
142   * @param collaborator the collaborator to add.
143   * @param role the role of the collaborator.
144   * @return info about the new collaboration.
145   */
146  public BoxCollaboration.Info collaborate(
147      BoxCollaborator collaborator, BoxCollaboration.Role role) {
148    JsonObject accessibleByField = new JsonObject();
149    accessibleByField.add("id", collaborator.getID());
150
151    if (collaborator instanceof BoxUser) {
152      accessibleByField.add("type", "user");
153    } else if (collaborator instanceof BoxGroup) {
154      accessibleByField.add("type", "group");
155    } else {
156      throw new IllegalArgumentException("The given collaborator is of an unknown type.");
157    }
158
159    return this.collaborate(accessibleByField, role, null, null, null, null);
160  }
161
162  /**
163   * Adds a collaborator to this folder. An email will be sent to the collaborator if they don't
164   * already have a Box account.
165   *
166   * @param email the email address of the collaborator to add.
167   * @param role the role of the collaborator.
168   * @return info about the new collaboration.
169   */
170  public BoxCollaboration.Info collaborate(String email, BoxCollaboration.Role role) {
171    JsonObject accessibleByField = new JsonObject();
172    accessibleByField.add("login", email);
173    accessibleByField.add("type", "user");
174
175    return this.collaborate(accessibleByField, role, null, null, null, null);
176  }
177
178  /**
179   * Adds a collaborator to this folder.
180   *
181   * @param collaborator the collaborator to add.
182   * @param role the role of the collaborator.
183   * @param notify the user/group should receive email notification of the collaboration or not.
184   * @param canViewPath the view path collaboration feature is enabled or not. View path
185   *     collaborations allow the invitee to see the entire ancestral path to the associated folder.
186   *     The user will not gain privileges in any ancestral folder.
187   * @param expiresAt when the collaboration should expire.
188   * @param isAccessOnly whether the collaboration is access only or not.
189   * @return info about the new collaboration.
190   */
191  public BoxCollaboration.Info collaborate(
192      BoxCollaborator collaborator,
193      BoxCollaboration.Role role,
194      Boolean notify,
195      Boolean canViewPath,
196      Date expiresAt,
197      Boolean isAccessOnly) {
198    JsonObject accessibleByField = new JsonObject();
199    accessibleByField.add("id", collaborator.getID());
200
201    if (collaborator instanceof BoxUser) {
202      accessibleByField.add("type", "user");
203    } else if (collaborator instanceof BoxGroup) {
204      accessibleByField.add("type", "group");
205    } else {
206      throw new IllegalArgumentException("The given collaborator is of an unknown type.");
207    }
208
209    return this.collaborate(accessibleByField, role, notify, canViewPath, expiresAt, isAccessOnly);
210  }
211
212  /**
213   * Adds a collaborator to this folder.
214   *
215   * @param collaborator the collaborator to add.
216   * @param role the role of the collaborator.
217   * @param notify the user/group should receive email notification of the collaboration or not.
218   * @param canViewPath the view path collaboration feature is enabled or not. View path
219   *     collaborations allow the invitee to see the entire ancestral path to the associated folder.
220   *     The user will not gain privileges in any ancestral folder.
221   * @return info about the new collaboration.
222   */
223  public BoxCollaboration.Info collaborate(
224      BoxCollaborator collaborator,
225      BoxCollaboration.Role role,
226      Boolean notify,
227      Boolean canViewPath) {
228    return this.collaborate(collaborator, role, notify, canViewPath, null, null);
229  }
230
231  /**
232   * Adds a collaborator to this folder. An email will be sent to the collaborator if they don't
233   * already have a Box account.
234   *
235   * @param email the email address of the collaborator to add.
236   * @param role the role of the collaborator.
237   * @param notify the user/group should receive email notification of the collaboration or not.
238   * @param canViewPath the view path collaboration feature is enabled or not. View path
239   *     collaborations allow the invitee to see the entire ancestral path to the associated folder.
240   *     The user will not gain privileges in any ancestral folder.
241   * @param expiresAt when the collaboration should expire.
242   * @param isAccessOnly whether the collaboration is access only or not.
243   * @return info about the new collaboration.
244   */
245  public BoxCollaboration.Info collaborate(
246      String email,
247      BoxCollaboration.Role role,
248      Boolean notify,
249      Boolean canViewPath,
250      Date expiresAt,
251      Boolean isAccessOnly) {
252    JsonObject accessibleByField = new JsonObject();
253    accessibleByField.add("login", email);
254    accessibleByField.add("type", "user");
255
256    return this.collaborate(accessibleByField, role, notify, canViewPath, expiresAt, isAccessOnly);
257  }
258
259  /**
260   * Adds a collaborator to this folder. An email will be sent to the collaborator if they don't
261   * already have a Box account.
262   *
263   * @param email the email address of the collaborator to add.
264   * @param role the role of the collaborator.
265   * @param notify the user/group should receive email notification of the collaboration or not.
266   * @param canViewPath the view path collaboration feature is enabled or not. View path
267   *     collaborations allow the invitee to see the entire ancestral path to the associated folder.
268   *     The user will not gain privileges in any ancestral folder.
269   * @return info about the new collaboration.
270   */
271  public BoxCollaboration.Info collaborate(
272      String email, BoxCollaboration.Role role, Boolean notify, Boolean canViewPath) {
273    return this.collaborate(email, role, notify, canViewPath, null, null);
274  }
275
276  private BoxCollaboration.Info collaborate(
277      JsonObject accessibleByField,
278      BoxCollaboration.Role role,
279      Boolean notify,
280      Boolean canViewPath,
281      Date expiresAt,
282      Boolean isAccessOnly) {
283
284    JsonObject itemField = new JsonObject();
285    itemField.add("id", this.getID());
286    itemField.add("type", "folder");
287
288    return BoxCollaboration.create(
289        this.getAPI(),
290        accessibleByField,
291        itemField,
292        role,
293        notify,
294        canViewPath,
295        expiresAt,
296        isAccessOnly);
297  }
298
299  /**
300   * Creates a shared link.
301   *
302   * @param sharedLinkRequest Shared link to create
303   * @return Created shared link.
304   */
305  public BoxSharedLink createSharedLink(BoxSharedLinkRequest sharedLinkRequest) {
306    return createSharedLink(sharedLinkRequest.asSharedLink());
307  }
308
309  private BoxSharedLink createSharedLink(BoxSharedLink sharedLink) {
310    BoxFolder.Info info = new BoxFolder.Info();
311    info.setSharedLink(removeCanEditPermissionIfSet(sharedLink));
312
313    this.updateInfo(info);
314    return info.getSharedLink();
315  }
316
317  private BoxSharedLink removeCanEditPermissionIfSet(BoxSharedLink sharedLink) {
318    if (sharedLink.getPermissions() != null && sharedLink.getPermissions().getCanEdit()) {
319      BoxSharedLink.Permissions permissions = sharedLink.getPermissions();
320      sharedLink.setPermissions(
321          new BoxSharedLink.Permissions(
322              permissions.getCanPreview(), permissions.getCanDownload(), false));
323    }
324    return sharedLink;
325  }
326
327  /**
328   * Gets information about all of the collaborations for this folder.
329   *
330   * @return a collection of information about the collaborations for this folder.
331   */
332  public Collection<BoxCollaboration.Info> getCollaborations(String... fields) {
333    BoxAPIConnection api = this.getAPI();
334    QueryStringBuilder queryBuilder = new QueryStringBuilder();
335    if (fields.length > 0) {
336      queryBuilder.appendParam("fields", fields);
337    }
338    URL url =
339        GET_COLLABORATIONS_URL.buildWithQuery(
340            api.getBaseURL(), queryBuilder.toString(), this.getID());
341
342    BoxJSONRequest request = new BoxJSONRequest(api, url, "GET");
343    try (BoxJSONResponse response = request.send()) {
344      JsonObject responseJSON = Json.parse(response.getJSON()).asObject();
345
346      int entriesCount = responseJSON.get("total_count").asInt();
347      Collection<BoxCollaboration.Info> collaborations = new ArrayList<>(entriesCount);
348      JsonArray entries = responseJSON.get("entries").asArray();
349      for (JsonValue entry : entries) {
350        JsonObject entryObject = entry.asObject();
351        BoxCollaboration collaboration =
352            new BoxCollaboration(api, entryObject.get("id").asString());
353        BoxCollaboration.Info info = collaboration.new Info(entryObject);
354        collaborations.add(info);
355      }
356
357      return collaborations;
358    }
359  }
360
361  @Override
362  public BoxFolder.Info getInfo(String... fields) {
363    URL url = FOLDER_INFO_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
364    if (fields.length > 0) {
365      String queryString = new QueryStringBuilder().appendParam("fields", fields).toString();
366      url =
367          FOLDER_INFO_URL_TEMPLATE.buildWithQuery(
368              this.getAPI().getBaseURL(), queryString, this.getID());
369    }
370
371    BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "GET");
372    try (BoxJSONResponse response = request.send()) {
373      return new Info(response.getJSON());
374    }
375  }
376
377  /**
378   * Updates the information about this folder with any info fields that have been modified locally.
379   *
380   * @param info the updated info.
381   */
382  public void updateInfo(BoxFolder.Info info) {
383    URL url = FOLDER_INFO_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
384    BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "PUT");
385    request.setBody(info.getPendingChanges());
386    try (BoxJSONResponse response = request.send()) {
387      JsonObject jsonObject = Json.parse(response.getJSON()).asObject();
388      info.update(jsonObject);
389    }
390  }
391
392  @Override
393  public BoxFolder.Info copy(BoxFolder destination) {
394    return this.copy(destination, null);
395  }
396
397  @Override
398  public BoxFolder.Info copy(BoxFolder destination, String newName) {
399    URL url = COPY_FOLDER_URL.build(this.getAPI().getBaseURL(), this.getID());
400    BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "POST");
401
402    JsonObject parent = new JsonObject();
403    parent.add("id", destination.getID());
404
405    JsonObject copyInfo = new JsonObject();
406    copyInfo.add("parent", parent);
407    if (newName != null) {
408      copyInfo.add("name", newName);
409    }
410
411    request.setBody(copyInfo.toString());
412    try (BoxJSONResponse response = request.send()) {
413      JsonObject responseJSON = Json.parse(response.getJSON()).asObject();
414      BoxFolder copiedFolder = new BoxFolder(this.getAPI(), responseJSON.get("id").asString());
415      return copiedFolder.new Info(responseJSON);
416    }
417  }
418
419  /**
420   * Creates a new child folder inside this folder.
421   *
422   * @param name the new folder's name.
423   * @return the created folder's info.
424   */
425  public BoxFolder.Info createFolder(String name) {
426    JsonObject parent = new JsonObject();
427    parent.add("id", this.getID());
428
429    JsonObject newFolder = new JsonObject();
430    newFolder.add("name", name);
431    newFolder.add("parent", parent);
432
433    BoxJSONRequest request =
434        new BoxJSONRequest(
435            this.getAPI(), CREATE_FOLDER_URL.build(this.getAPI().getBaseURL()), "POST");
436    request.setBody(newFolder.toString());
437    try (BoxJSONResponse response = request.send()) {
438      JsonObject responseJSON = Json.parse(response.getJSON()).asObject();
439
440      BoxFolder createdFolder = new BoxFolder(this.getAPI(), responseJSON.get("id").asString());
441      return createdFolder.new Info(responseJSON);
442    }
443  }
444
445  /**
446   * Deletes this folder, optionally recursively deleting all of its contents.
447   *
448   * @param recursive true to recursively delete this folder's contents; otherwise false.
449   */
450  public void delete(boolean recursive) {
451    URL url = DELETE_FOLDER_URL.buildAlpha(this.getAPI().getBaseURL(), this.getID(), recursive);
452    BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "DELETE");
453    request.send().close();
454  }
455
456  @Override
457  public BoxItem.Info move(BoxFolder destination) {
458    return this.move(destination, null);
459  }
460
461  @Override
462  public BoxItem.Info move(BoxFolder destination, String newName) {
463    URL url = FOLDER_INFO_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
464    BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "PUT");
465
466    JsonObject parent = new JsonObject();
467    parent.add("id", destination.getID());
468
469    JsonObject updateInfo = new JsonObject();
470    updateInfo.add("parent", parent);
471    if (newName != null) {
472      updateInfo.add("name", newName);
473    }
474
475    request.setBody(updateInfo.toString());
476    try (BoxJSONResponse response = request.send()) {
477      JsonObject responseJSON = Json.parse(response.getJSON()).asObject();
478      BoxFolder movedFolder = new BoxFolder(this.getAPI(), responseJSON.get("id").asString());
479      return movedFolder.new Info(responseJSON);
480    }
481  }
482
483  /**
484   * Renames this folder.
485   *
486   * @param newName the new name of the folder.
487   */
488  public void rename(String newName) {
489    URL url = FOLDER_INFO_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
490    BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "PUT");
491
492    JsonObject updateInfo = new JsonObject();
493    updateInfo.add("name", newName);
494
495    request.setBody(updateInfo.toString());
496    try (BoxJSONResponse response = request.send()) {
497      response.getJSON();
498    }
499  }
500
501  /**
502   * Checks if the file can be successfully uploaded by using the preflight check.
503   *
504   * @param name the name to give the uploaded file.
505   * @param fileSize the size of the file used for account capacity calculations.
506   */
507  public void canUpload(String name, long fileSize) {
508    URL url = UPLOAD_FILE_URL.build(this.getAPI().getBaseURL());
509    BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "OPTIONS");
510
511    JsonObject parent = new JsonObject();
512    parent.add("id", this.getID());
513
514    JsonObject preflightInfo = new JsonObject();
515    preflightInfo.add("parent", parent);
516    preflightInfo.add("name", name);
517
518    preflightInfo.add("size", fileSize);
519
520    request.setBody(preflightInfo.toString());
521    try (BoxJSONResponse response = request.send()) {
522      response.getJSON();
523    }
524  }
525
526  /**
527   * Uploads a new file to this folder.
528   *
529   * @param fileContent a stream containing the contents of the file to upload.
530   * @param name the name to give the uploaded file.
531   * @return the uploaded file's info.
532   */
533  public BoxFile.Info uploadFile(InputStream fileContent, String name) {
534    FileUploadParams uploadInfo = new FileUploadParams().setContent(fileContent).setName(name);
535    return this.uploadFile(uploadInfo);
536  }
537
538  /**
539   * Uploads a new file to this folder.
540   *
541   * @param callback the callback which allows file content to be written on output stream.
542   * @param name the name to give the uploaded file.
543   * @return the uploaded file's info.
544   */
545  public BoxFile.Info uploadFile(UploadFileCallback callback, String name) {
546    FileUploadParams uploadInfo =
547        new FileUploadParams().setUploadFileCallback(callback).setName(name);
548    return this.uploadFile(uploadInfo);
549  }
550
551  /**
552   * Uploads a new file to this folder while reporting the progress to a ProgressListener.
553   *
554   * @param fileContent a stream containing the contents of the file to upload.
555   * @param name the name to give the uploaded file.
556   * @param fileSize the size of the file used for determining the progress of the upload.
557   * @param listener a listener for monitoring the upload's progress.
558   * @return the uploaded file's info.
559   */
560  public BoxFile.Info uploadFile(
561      InputStream fileContent, String name, long fileSize, ProgressListener listener) {
562    FileUploadParams uploadInfo =
563        new FileUploadParams()
564            .setContent(fileContent)
565            .setName(name)
566            .setSize(fileSize)
567            .setProgressListener(listener);
568    return this.uploadFile(uploadInfo);
569  }
570
571  /**
572   * Uploads a new file to this folder with a specified file description.
573   *
574   * @param fileContent a stream containing the contents of the file to upload.
575   * @param name the name to give the uploaded file.
576   * @param description the description to give the uploaded file.
577   * @return the uploaded file's info.
578   */
579  public BoxFile.Info uploadFile(InputStream fileContent, String name, String description) {
580    FileUploadParams uploadInfo =
581        new FileUploadParams().setContent(fileContent).setName(name).setDescription(description);
582    return this.uploadFile(uploadInfo);
583  }
584
585  /**
586   * Uploads a new file to this folder with custom upload parameters.
587   *
588   * @param uploadParams the custom upload parameters.
589   * @return the uploaded file's info.
590   */
591  public BoxFile.Info uploadFile(FileUploadParams uploadParams) {
592    URL uploadURL = UPLOAD_FILE_URL.build(this.getAPI().getBaseUploadURL());
593    BoxMultipartRequest request = new BoxMultipartRequest(getAPI(), uploadURL);
594
595    JsonObject fieldJSON = new JsonObject();
596    JsonObject parentIdJSON = new JsonObject();
597    parentIdJSON.add("id", getID());
598    fieldJSON.add("name", uploadParams.getName());
599    fieldJSON.add("parent", parentIdJSON);
600
601    if (uploadParams.getCreated() != null) {
602      fieldJSON.add("content_created_at", BoxDateFormat.format(uploadParams.getCreated()));
603    }
604
605    if (uploadParams.getModified() != null) {
606      fieldJSON.add("content_modified_at", BoxDateFormat.format(uploadParams.getModified()));
607    }
608
609    if (uploadParams.getSHA1() != null && !uploadParams.getSHA1().isEmpty()) {
610      request.setContentSHA1(uploadParams.getSHA1());
611    }
612
613    if (uploadParams.getDescription() != null) {
614      fieldJSON.add("description", uploadParams.getDescription());
615    }
616
617    request.putField("attributes", fieldJSON.toString());
618
619    if (uploadParams.getSize() > 0) {
620      request.setFile(uploadParams.getContent(), uploadParams.getName(), uploadParams.getSize());
621    } else if (uploadParams.getContent() != null) {
622      request.setFile(uploadParams.getContent(), uploadParams.getName());
623    } else {
624      request.setUploadFileCallback(uploadParams.getUploadFileCallback(), uploadParams.getName());
625    }
626
627    BoxJSONResponse response = null;
628    try {
629      if (uploadParams.getProgressListener() == null) {
630        // upload files sends multipart request but response is JSON
631        response = (BoxJSONResponse) request.send();
632      } else {
633        // upload files sends multipart request but response is JSON
634        response = (BoxJSONResponse) request.send(uploadParams.getProgressListener());
635      }
636      JsonObject collection = Json.parse(response.getJSON()).asObject();
637      JsonArray entries = collection.get("entries").asArray();
638      JsonObject fileInfoJSON = entries.get(0).asObject();
639      String uploadedFileID = fileInfoJSON.get("id").asString();
640
641      BoxFile uploadedFile = new BoxFile(getAPI(), uploadedFileID);
642      return uploadedFile.new Info(fileInfoJSON);
643    } finally {
644      Optional.ofNullable(response).ifPresent(BoxAPIResponse::close);
645    }
646  }
647
648  /**
649   * Uploads a new weblink to this folder.
650   *
651   * @param linkURL the URL the weblink points to.
652   * @return the uploaded weblink's info.
653   */
654  public BoxWebLink.Info createWebLink(URL linkURL) {
655    return this.createWebLink(null, linkURL, null);
656  }
657
658  /**
659   * Uploads a new weblink to this folder.
660   *
661   * @param name the filename for the weblink.
662   * @param linkURL the URL the weblink points to.
663   * @return the uploaded weblink's info.
664   */
665  public BoxWebLink.Info createWebLink(String name, URL linkURL) {
666    return this.createWebLink(name, linkURL, null);
667  }
668
669  /**
670   * Uploads a new weblink to this folder.
671   *
672   * @param linkURL the URL the weblink points to.
673   * @param description the weblink's description.
674   * @return the uploaded weblink's info.
675   */
676  public BoxWebLink.Info createWebLink(URL linkURL, String description) {
677    return this.createWebLink(null, linkURL, description);
678  }
679
680  /**
681   * Uploads a new weblink to this folder.
682   *
683   * @param name the filename for the weblink.
684   * @param linkURL the URL the weblink points to.
685   * @param description the weblink's description.
686   * @return the uploaded weblink's info.
687   */
688  public BoxWebLink.Info createWebLink(String name, URL linkURL, String description) {
689    JsonObject parent = new JsonObject();
690    parent.add("id", this.getID());
691
692    JsonObject newWebLink = new JsonObject();
693    newWebLink.add("name", name);
694    newWebLink.add("parent", parent);
695    newWebLink.add("url", linkURL.toString());
696
697    if (description != null) {
698      newWebLink.add("description", description);
699    }
700
701    BoxJSONRequest request =
702        new BoxJSONRequest(
703            this.getAPI(), CREATE_WEB_LINK_URL.build(this.getAPI().getBaseURL()), "POST");
704    request.setBody(newWebLink.toString());
705    try (BoxJSONResponse response = request.send()) {
706      JsonObject responseJSON = Json.parse(response.getJSON()).asObject();
707
708      BoxWebLink createdWebLink = new BoxWebLink(this.getAPI(), responseJSON.get("id").asString());
709      return createdWebLink.new Info(responseJSON);
710    }
711  }
712
713  /**
714   * Returns an iterable containing the items in this folder. Iterating over the iterable returned
715   * by this method is equivalent to iterating over this BoxFolder directly.
716   *
717   * @return an iterable containing the items in this folder.
718   */
719  public Iterable<BoxItem.Info> getChildren() {
720    return this;
721  }
722
723  /**
724   * Returns an iterable containing the items in this folder and specifies which child fields to
725   * retrieve from the API.
726   *
727   * @param fields the fields to retrieve.
728   * @return an iterable containing the items in this folder.
729   */
730  public Iterable<BoxItem.Info> getChildren(final String... fields) {
731    return () -> {
732      String queryString = new QueryStringBuilder().appendParam("fields", fields).toString();
733      URL url = GET_ITEMS_URL.buildWithQuery(getAPI().getBaseURL(), queryString, getID());
734      return new BoxItemIterator(getAPI(), url, marker(DEFAULT_LIMIT));
735    };
736  }
737
738  /**
739   * Returns an iterable containing the items in this folder sorted by name and direction.
740   *
741   * @param sort the field to sort by, can be set as `name`, `id`, and `date`.
742   * @param direction the direction to display the item results.
743   * @param fields the fields to retrieve.
744   * @return an iterable containing the items in this folder.
745   */
746  public Iterable<BoxItem.Info> getChildren(
747      String sort, SortDirection direction, final String... fields) {
748    QueryStringBuilder builder =
749        new QueryStringBuilder()
750            .appendParam("sort", sort)
751            .appendParam("direction", direction.toString());
752
753    if (fields.length > 0) {
754      builder.appendParam("fields", fields);
755    }
756    final String query = builder.toString();
757    return () -> {
758      URL url = GET_ITEMS_URL.buildWithQuery(getAPI().getBaseURL(), query, getID());
759      return new BoxItemIterator(getAPI(), url, offset(0, DEFAULT_LIMIT));
760    };
761  }
762
763  /**
764   * Returns an iterable containing the items in this folder sorted by name and direction.
765   *
766   * @param sort the field to sort by, can be set as `name`, `id`, and `date`.
767   * @param direction the direction to display the item results.
768   * @param offset the index of the first child item to retrieve.
769   * @param limit the maximum number of children to retrieve after the offset.
770   * @param fields the fields to retrieve.
771   * @return an iterable containing the items in this folder.
772   */
773  public Iterable<BoxItem.Info> getChildren(
774      String sort,
775      SortDirection direction,
776      final long offset,
777      final long limit,
778      final String... fields) {
779    QueryStringBuilder builder =
780        new QueryStringBuilder()
781            .appendParam("sort", sort)
782            .appendParam("direction", direction.toString());
783
784    if (fields.length > 0) {
785      builder.appendParam("fields", fields);
786    }
787    final String query = builder.toString();
788    return () -> {
789      URL url = GET_ITEMS_URL.buildWithQuery(getAPI().getBaseURL(), query, getID());
790      return new BoxItemIterator(getAPI(), url, limit, offset);
791    };
792  }
793
794  /**
795   * Retrieves a specific range of child items in this folder.
796   *
797   * @param offset the index of the first child item to retrieve.
798   * @param limit the maximum number of children to retrieve after the offset.
799   * @param fields the fields to retrieve.
800   * @return a partial collection containing the specified range of child items.
801   */
802  public PartialCollection<BoxItem.Info> getChildrenRange(
803      long offset, long limit, String... fields) {
804    QueryStringBuilder builder =
805        new QueryStringBuilder().appendParam("limit", limit).appendParam("offset", offset);
806
807    if (fields.length > 0) {
808      builder.appendParam("fields", fields);
809    }
810
811    URL url = GET_ITEMS_URL.buildWithQuery(getAPI().getBaseURL(), builder.toString(), getID());
812    BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "GET");
813    try (BoxJSONResponse response = request.send()) {
814      JsonObject responseJSON = Json.parse(response.getJSON()).asObject();
815
816      String totalCountString = responseJSON.get("total_count").toString();
817      long fullSize = Double.valueOf(totalCountString).longValue();
818      PartialCollection<BoxItem.Info> children = new PartialCollection<>(offset, limit, fullSize);
819      JsonArray jsonArray = responseJSON.get("entries").asArray();
820      for (JsonValue value : jsonArray) {
821        JsonObject jsonObject = value.asObject();
822        BoxItem.Info parsedItemInfo =
823            (BoxItem.Info) BoxResource.parseInfo(this.getAPI(), jsonObject);
824        if (parsedItemInfo != null) {
825          children.add(parsedItemInfo);
826        }
827      }
828      return children;
829    }
830  }
831
832  /**
833   * Returns an iterable containing the items in this folder sorted by name and direction.
834   *
835   * @param sortParameters describes sorting parameters. Sort parameters are supported only with
836   *     offset based pagination. Use {@link SortParameters#none()} to ignore sorting.
837   * @param pagingParameters describes paging parameters.
838   * @param fields the fields to retrieve.
839   * @return an iterable containing the items in this folder.
840   */
841  public Iterable<BoxItem.Info> getChildren(
842      final SortParameters sortParameters,
843      final PagingParameters pagingParameters,
844      String... fields) {
845    QueryStringBuilder builder = sortParameters.asQueryStringBuilder();
846    validateSortIsSelectedWithOffsetPaginationOnly(pagingParameters, builder);
847
848    if (fields.length > 0) {
849      builder.appendParam("fields", fields);
850    }
851    final String query = builder.toString();
852    return () -> {
853      URL url = GET_ITEMS_URL.buildWithQuery(getAPI().getBaseURL(), query, getID());
854      return new BoxItemIterator(getAPI(), url, pagingParameters);
855    };
856  }
857
858  /**
859   * Returns an iterator over the items in this folder.
860   *
861   * @return an iterator over the items in this folder.
862   */
863  @Override
864  public Iterator<BoxItem.Info> iterator() {
865    URL url = GET_ITEMS_URL.build(this.getAPI().getBaseURL(), BoxFolder.this.getID());
866    return new BoxItemIterator(BoxFolder.this.getAPI(), url, marker(DEFAULT_LIMIT));
867  }
868
869  /**
870   * Adds new {@link BoxWebHook} to this {@link BoxFolder}.
871   *
872   * @param address {@link BoxWebHook.Info#getAddress()}
873   * @param triggers {@link BoxWebHook.Info#getTriggers()}
874   * @return created {@link BoxWebHook.Info}
875   */
876  public BoxWebHook.Info addWebHook(URL address, BoxWebHook.Trigger... triggers) {
877    return BoxWebHook.create(this, address, triggers);
878  }
879
880  /**
881   * Used to retrieve the watermark for the folder. If the folder does not have a watermark applied
882   * to it, a 404 Not Found will be returned by API.
883   *
884   * @param fields the fields to retrieve.
885   * @return the watermark associated with the folder.
886   */
887  public BoxWatermark getWatermark(String... fields) {
888    return this.getWatermark(FOLDER_INFO_URL_TEMPLATE, fields);
889  }
890
891  /**
892   * Used to apply or update the watermark for the folder.
893   *
894   * @return the watermark associated with the folder.
895   */
896  public BoxWatermark applyWatermark() {
897    return this.applyWatermark(FOLDER_INFO_URL_TEMPLATE, BoxWatermark.WATERMARK_DEFAULT_IMPRINT);
898  }
899
900  /**
901   * Removes a watermark from the folder. If the folder did not have a watermark applied to it, a
902   * 404 Not Found will be returned by API.
903   */
904  public void removeWatermark() {
905    this.removeWatermark(FOLDER_INFO_URL_TEMPLATE);
906  }
907
908  /**
909   * Used to retrieve all metadata associated with the folder.
910   *
911   * @param fields the optional fields to retrieve.
912   * @return An iterable of metadata instances associated with the folder
913   */
914  public Iterable<Metadata> getAllMetadata(String... fields) {
915    return Metadata.getAllMetadata(this, fields);
916  }
917
918  @Override
919  public BoxFolder.Info setCollections(BoxCollection... collections) {
920    JsonArray jsonArray = new JsonArray();
921    for (BoxCollection collection : collections) {
922      JsonObject collectionJSON = new JsonObject();
923      collectionJSON.add("id", collection.getID());
924      jsonArray.add(collectionJSON);
925    }
926    JsonObject infoJSON = new JsonObject();
927    infoJSON.add("collections", jsonArray);
928
929    String queryString = new QueryStringBuilder().appendParam("fields", ALL_FIELDS).toString();
930    URL url =
931        FOLDER_INFO_URL_TEMPLATE.buildWithQuery(
932            this.getAPI().getBaseURL(), queryString, this.getID());
933    BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "PUT");
934    request.setBody(infoJSON.toString());
935    try (BoxJSONResponse response = request.send()) {
936      JsonObject jsonObject = Json.parse(response.getJSON()).asObject();
937      return new Info(jsonObject);
938    }
939  }
940
941  /**
942   * Creates global property metadata on this folder.
943   *
944   * @param metadata the new metadata values.
945   * @return the metadata returned from the server.
946   */
947  public Metadata createMetadata(Metadata metadata) {
948    return this.createMetadata(Metadata.DEFAULT_METADATA_TYPE, metadata);
949  }
950
951  /**
952   * Creates metadata on this folder using a specified template.
953   *
954   * @param templateName the name of the metadata template.
955   * @param metadata the new metadata values.
956   * @return the metadata returned from the server.
957   */
958  public Metadata createMetadata(String templateName, Metadata metadata) {
959    String scope = Metadata.scopeBasedOnType(templateName);
960    return this.createMetadata(templateName, scope, metadata);
961  }
962
963  /**
964   * Creates metadata on this folder using a specified scope and template.
965   *
966   * @param templateName the name of the metadata template.
967   * @param scope the scope of the template (usually "global" or "enterprise").
968   * @param metadata the new metadata values.
969   * @return the metadata returned from the server.
970   */
971  public Metadata createMetadata(String templateName, String scope, Metadata metadata) {
972    URL url =
973        METADATA_URL_TEMPLATE.buildAlpha(
974            this.getAPI().getBaseURL(), this.getID(), scope, templateName);
975    BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "POST");
976    request.setBody(metadata.toString());
977    try (BoxJSONResponse response = request.send()) {
978      return new Metadata(Json.parse(response.getJSON()).asObject());
979    }
980  }
981
982  /**
983   * Sets the provided metadata on the folder. If metadata has already been created on this folder,
984   * it overwrites metadata keys specified in the `metadata` param.
985   *
986   * @param templateName the name of the metadata template.
987   * @param scope the scope of the template (usually "global" or "enterprise").
988   * @param metadata the new metadata values.
989   * @return the metadata returned from the server.
990   */
991  public Metadata setMetadata(String templateName, String scope, Metadata metadata) {
992    try {
993      return this.createMetadata(templateName, scope, metadata);
994    } catch (BoxAPIException e) {
995      if (e.getResponseCode() == 409) {
996        if (metadata.getOperations().isEmpty()) {
997          return getMetadata();
998        } else {
999          return updateExistingTemplate(templateName, scope, metadata);
1000        }
1001      } else {
1002        throw e;
1003      }
1004    }
1005  }
1006
1007  /**
1008   * Throws IllegalArgumentException exception when sorting and marker pagination is selected.
1009   *
1010   * @param pagingParameters paging definition to check
1011   * @param sortQuery builder containing sort query
1012   */
1013  private void validateSortIsSelectedWithOffsetPaginationOnly(
1014      PagingParameters pagingParameters, QueryStringBuilder sortQuery) {
1015    if (pagingParameters != null
1016        && pagingParameters.isMarkerBasedPaging()
1017        && sortQuery.toString().length() > 0) {
1018      throw new IllegalArgumentException(
1019          "Sorting is not supported when using marker based pagination.");
1020    }
1021  }
1022
1023  private Metadata updateExistingTemplate(String templateName, String scope, Metadata metadata) {
1024    Metadata metadataToUpdate = new Metadata(scope, templateName);
1025    for (JsonValue value : metadata.getOperations()) {
1026      if (value.asObject().get("value").isNumber()) {
1027        metadataToUpdate.add(
1028            value.asObject().get("path").asString(), value.asObject().get("value").asDouble());
1029      } else if (value.asObject().get("value").isString()) {
1030        metadataToUpdate.add(
1031            value.asObject().get("path").asString(), value.asObject().get("value").asString());
1032      } else if (value.asObject().get("value").isArray()) {
1033        ArrayList<String> list = new ArrayList<>();
1034        for (JsonValue jsonValue : value.asObject().get("value").asArray()) {
1035          list.add(jsonValue.asString());
1036        }
1037        metadataToUpdate.add(value.asObject().get("path").asString(), list);
1038      }
1039    }
1040    return this.updateMetadata(metadataToUpdate);
1041  }
1042
1043  /**
1044   * Gets the global properties metadata on this folder.
1045   *
1046   * @return the metadata returned from the server.
1047   */
1048  public Metadata getMetadata() {
1049    return this.getMetadata(Metadata.DEFAULT_METADATA_TYPE);
1050  }
1051
1052  /**
1053   * Gets the metadata on this folder associated with a specified template.
1054   *
1055   * @param templateName the metadata template type name.
1056   * @return the metadata returned from the server.
1057   */
1058  public Metadata getMetadata(String templateName) {
1059    String scope = Metadata.scopeBasedOnType(templateName);
1060    return this.getMetadata(templateName, scope);
1061  }
1062
1063  /**
1064   * Gets the metadata on this folder associated with a specified scope and template.
1065   *
1066   * @param templateName the metadata template type name.
1067   * @param scope the scope of the template (usually "global" or "enterprise").
1068   * @return the metadata returned from the server.
1069   */
1070  public Metadata getMetadata(String templateName, String scope) {
1071    URL url =
1072        METADATA_URL_TEMPLATE.buildAlpha(
1073            this.getAPI().getBaseURL(), this.getID(), scope, templateName);
1074    BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "GET");
1075    try (BoxJSONResponse response = request.send()) {
1076      return new Metadata(Json.parse(response.getJSON()).asObject());
1077    }
1078  }
1079
1080  /**
1081   * Updates the folder metadata.
1082   *
1083   * @param metadata the new metadata values.
1084   * @return the metadata returned from the server.
1085   */
1086  public Metadata updateMetadata(Metadata metadata) {
1087    URL url =
1088        METADATA_URL_TEMPLATE.buildAlpha(
1089            this.getAPI().getBaseURL(),
1090            this.getID(),
1091            metadata.getScope(),
1092            metadata.getTemplateName());
1093    BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "PUT", APPLICATION_JSON_PATCH);
1094    request.setBody(metadata.getPatch());
1095    try (BoxJSONResponse response = request.send()) {
1096      return new Metadata(Json.parse(response.getJSON()).asObject());
1097    }
1098  }
1099
1100  /** Deletes the global properties metadata on this folder. */
1101  public void deleteMetadata() {
1102    this.deleteMetadata(Metadata.DEFAULT_METADATA_TYPE);
1103  }
1104
1105  /**
1106   * Deletes the metadata on this folder associated with a specified template.
1107   *
1108   * @param templateName the metadata template type name.
1109   */
1110  public void deleteMetadata(String templateName) {
1111    String scope = Metadata.scopeBasedOnType(templateName);
1112    this.deleteMetadata(templateName, scope);
1113  }
1114
1115  /**
1116   * Deletes the metadata on this folder associated with a specified scope and template.
1117   *
1118   * @param templateName the metadata template type name.
1119   * @param scope the scope of the template (usually "global" or "enterprise").
1120   */
1121  public void deleteMetadata(String templateName, String scope) {
1122    URL url =
1123        METADATA_URL_TEMPLATE.buildAlpha(
1124            this.getAPI().getBaseURL(), this.getID(), scope, templateName);
1125    BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "DELETE");
1126    request.send().close();
1127  }
1128
1129  /**
1130   * Adds a metadata classification to the specified file.
1131   *
1132   * @param classificationType the metadata classification type.
1133   * @return the metadata classification type added to the file.
1134   */
1135  public String addClassification(String classificationType) {
1136    Metadata metadata = new Metadata().add(Metadata.CLASSIFICATION_KEY, classificationType);
1137    Metadata classification =
1138        this.createMetadata(Metadata.CLASSIFICATION_TEMPLATE_KEY, "enterprise", metadata);
1139
1140    return classification.getString(Metadata.CLASSIFICATION_KEY);
1141  }
1142
1143  /**
1144   * Updates a metadata classification on the specified file.
1145   *
1146   * @param classificationType the metadata classification type.
1147   * @return the new metadata classification type updated on the file.
1148   */
1149  public String updateClassification(String classificationType) {
1150    Metadata metadata = new Metadata("enterprise", Metadata.CLASSIFICATION_TEMPLATE_KEY);
1151    metadata.replace(Metadata.CLASSIFICATION_KEY, classificationType);
1152    Metadata classification = this.updateMetadata(metadata);
1153
1154    return classification.getString(Metadata.CLASSIFICATION_KEY);
1155  }
1156
1157  /**
1158   * Attempts to add classification to a file. If classification already exists then do update.
1159   *
1160   * @param classificationType the metadata classification type.
1161   * @return the metadata classification type on the file.
1162   */
1163  public String setClassification(String classificationType) {
1164    Metadata metadata = new Metadata().add(Metadata.CLASSIFICATION_KEY, classificationType);
1165    Metadata classification;
1166
1167    try {
1168      classification =
1169          this.createMetadata(Metadata.CLASSIFICATION_TEMPLATE_KEY, "enterprise", metadata);
1170    } catch (BoxAPIException e) {
1171      if (e.getResponseCode() == 409) {
1172        metadata = new Metadata("enterprise", Metadata.CLASSIFICATION_TEMPLATE_KEY);
1173        metadata.replace(Metadata.CLASSIFICATION_KEY, classificationType);
1174        classification = this.updateMetadata(metadata);
1175      } else {
1176        throw e;
1177      }
1178    }
1179
1180    return classification.getString("/Box__Security__Classification__Key");
1181  }
1182
1183  /**
1184   * Gets the classification type for the specified file.
1185   *
1186   * @return the metadata classification type on the file.
1187   */
1188  public String getClassification() {
1189    Metadata metadata = this.getMetadata(Metadata.CLASSIFICATION_TEMPLATE_KEY);
1190    return metadata.getString(Metadata.CLASSIFICATION_KEY);
1191  }
1192
1193  /** Deletes the classification on the file. */
1194  public void deleteClassification() {
1195    this.deleteMetadata(Metadata.CLASSIFICATION_TEMPLATE_KEY, "enterprise");
1196  }
1197
1198  /**
1199   * Creates an upload session to create a new file in chunks. This will first verify that the file
1200   * can be created and then open a session for uploading pieces of the file.
1201   *
1202   * @param fileName the name of the file to be created
1203   * @param fileSize the size of the file that will be uploaded
1204   * @return the created upload session instance
1205   */
1206  public BoxFileUploadSession.Info createUploadSession(String fileName, long fileSize) {
1207
1208    URL url = UPLOAD_SESSION_URL_TEMPLATE.build(this.getAPI().getBaseUploadURL());
1209    BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "POST");
1210
1211    JsonObject body = new JsonObject();
1212    body.add("folder_id", this.getID());
1213    body.add("file_name", fileName);
1214    body.add("file_size", fileSize);
1215    request.setBody(body.toString());
1216
1217    try (BoxJSONResponse response = request.send()) {
1218      JsonObject jsonObject = Json.parse(response.getJSON()).asObject();
1219
1220      String sessionId = jsonObject.get("id").asString();
1221      BoxFileUploadSession session = new BoxFileUploadSession(this.getAPI(), sessionId);
1222
1223      return session.new Info(jsonObject);
1224    }
1225  }
1226
1227  /**
1228   * Creates a new file.
1229   *
1230   * @param inputStream the stream instance that contains the data.
1231   * @param fileName the name of the file to be created.
1232   * @param fileSize the size of the file that will be uploaded.
1233   * @return the created file instance.
1234   * @throws InterruptedException when a thread execution is interrupted.
1235   * @throws IOException when reading a stream throws exception.
1236   */
1237  public BoxFile.Info uploadLargeFile(InputStream inputStream, String fileName, long fileSize)
1238      throws InterruptedException, IOException {
1239    URL url = UPLOAD_SESSION_URL_TEMPLATE.build(this.getAPI().getBaseUploadURL());
1240    this.canUpload(fileName, fileSize);
1241    return new LargeFileUpload()
1242        .upload(this.getAPI(), this.getID(), inputStream, url, fileName, fileSize);
1243  }
1244
1245  /**
1246   * Creates a new file. Also sets file attributes.
1247   *
1248   * @param inputStream the stream instance that contains the data.
1249   * @param fileName the name of the file to be created.
1250   * @param fileSize the size of the file that will be uploaded.
1251   * @param fileAttributes file attributes to set
1252   * @return the created file instance.
1253   * @throws InterruptedException when a thread execution is interrupted.
1254   * @throws IOException when reading a stream throws exception.
1255   */
1256  public BoxFile.Info uploadLargeFile(
1257      InputStream inputStream, String fileName, long fileSize, Map<String, String> fileAttributes)
1258      throws InterruptedException, IOException {
1259    URL url = UPLOAD_SESSION_URL_TEMPLATE.build(this.getAPI().getBaseUploadURL());
1260    this.canUpload(fileName, fileSize);
1261    return new LargeFileUpload()
1262        .upload(this.getAPI(), this.getID(), inputStream, url, fileName, fileSize, fileAttributes);
1263  }
1264
1265  /**
1266   * Creates a new file using specified number of parallel http connections.
1267   *
1268   * @param inputStream the stream instance that contains the data.
1269   * @param fileName the name of the file to be created.
1270   * @param fileSize the size of the file that will be uploaded.
1271   * @param nParallelConnections number of parallel http connections to use
1272   * @param timeOut time to wait before killing the job
1273   * @param unit time unit for the time wait value
1274   * @return the created file instance.
1275   * @throws InterruptedException when a thread execution is interrupted.
1276   * @throws IOException when reading a stream throws exception.
1277   */
1278  public BoxFile.Info uploadLargeFile(
1279      InputStream inputStream,
1280      String fileName,
1281      long fileSize,
1282      int nParallelConnections,
1283      long timeOut,
1284      TimeUnit unit)
1285      throws InterruptedException, IOException {
1286    URL url = UPLOAD_SESSION_URL_TEMPLATE.build(this.getAPI().getBaseUploadURL());
1287    this.canUpload(fileName, fileSize);
1288    return new LargeFileUpload(nParallelConnections, timeOut, unit)
1289        .upload(this.getAPI(), this.getID(), inputStream, url, fileName, fileSize);
1290  }
1291
1292  /**
1293   * Creates a new file using specified number of parallel http connections. Also sets file
1294   * attributes.
1295   *
1296   * @param inputStream the stream instance that contains the data.
1297   * @param fileName the name of the file to be created.
1298   * @param fileSize the size of the file that will be uploaded.
1299   * @param nParallelConnections number of parallel http connections to use
1300   * @param timeOut time to wait before killing the job
1301   * @param unit time unit for the time wait value
1302   * @param fileAttributes file attributes to set
1303   * @return the created file instance.
1304   * @throws InterruptedException when a thread execution is interrupted.
1305   * @throws IOException when reading a stream throws exception.
1306   */
1307  public BoxFile.Info uploadLargeFile(
1308      InputStream inputStream,
1309      String fileName,
1310      long fileSize,
1311      int nParallelConnections,
1312      long timeOut,
1313      TimeUnit unit,
1314      Map<String, String> fileAttributes)
1315      throws InterruptedException, IOException {
1316    URL url = UPLOAD_SESSION_URL_TEMPLATE.build(this.getAPI().getBaseUploadURL());
1317    this.canUpload(fileName, fileSize);
1318    return new LargeFileUpload(nParallelConnections, timeOut, unit)
1319        .upload(this.getAPI(), this.getID(), inputStream, url, fileName, fileSize, fileAttributes);
1320  }
1321
1322  /**
1323   * Creates a new Metadata Cascade Policy on a folder.
1324   *
1325   * @param scope the scope of the metadata cascade policy.
1326   * @param templateKey the key of the template.
1327   * @return information about the Metadata Cascade Policy.
1328   */
1329  public BoxMetadataCascadePolicy.Info addMetadataCascadePolicy(String scope, String templateKey) {
1330
1331    return BoxMetadataCascadePolicy.create(this.getAPI(), this.getID(), scope, templateKey);
1332  }
1333
1334  /**
1335   * Retrieves all Metadata Cascade Policies on a folder.
1336   *
1337   * @param fields optional fields to retrieve for cascade policies.
1338   * @return the Iterable of Box Metadata Cascade Policies in your enterprise.
1339   */
1340  public Iterable<BoxMetadataCascadePolicy.Info> getMetadataCascadePolicies(String... fields) {
1341    return BoxMetadataCascadePolicy.getAll(this.getAPI(), this.getID(), fields);
1342  }
1343
1344  /**
1345   * Retrieves all Metadata Cascade Policies on a folder.
1346   *
1347   * @param enterpriseID the ID of the enterprise to retrieve cascade policies for.
1348   * @param limit the number of entries of cascade policies to retrieve.
1349   * @param fields optional fields to retrieve for cascade policies.
1350   * @return the Iterable of Box Metadata Cascade Policies in your enterprise.
1351   */
1352  public Iterable<BoxMetadataCascadePolicy.Info> getMetadataCascadePolicies(
1353      String enterpriseID, int limit, String... fields) {
1354
1355    return BoxMetadataCascadePolicy.getAll(
1356        this.getAPI(), this.getID(), enterpriseID, limit, fields);
1357  }
1358
1359  /**
1360   * Lock this folder.
1361   *
1362   * @return a created folder lock object.
1363   */
1364  public BoxFolderLock.Info lock() {
1365    JsonObject folderObject = new JsonObject();
1366    folderObject.add("type", "folder");
1367    folderObject.add("id", this.getID());
1368
1369    JsonObject lockedOperations = new JsonObject();
1370    lockedOperations.add("move", true);
1371    lockedOperations.add("delete", true);
1372
1373    JsonObject body = new JsonObject();
1374    body.add("folder", folderObject);
1375    body.add("locked_operations", lockedOperations);
1376
1377    BoxJSONRequest request =
1378        new BoxJSONRequest(
1379            this.getAPI(), FOLDER_LOCK_URL_TEMPLATE.build(this.getAPI().getBaseURL()), "POST");
1380    request.setBody(body.toString());
1381    try (BoxJSONResponse response = request.send()) {
1382      JsonObject responseJSON = Json.parse(response.getJSON()).asObject();
1383
1384      BoxFolderLock createdFolderLock =
1385          new BoxFolderLock(this.getAPI(), responseJSON.get("id").asString());
1386      return createdFolderLock.new Info(responseJSON);
1387    }
1388  }
1389
1390  /**
1391   * Get the lock on this folder.
1392   *
1393   * @return a folder lock object.
1394   */
1395  public Iterable<BoxFolderLock.Info> getLocks() {
1396    String queryString = new QueryStringBuilder().appendParam("folder_id", this.getID()).toString();
1397    final BoxAPIConnection api = this.getAPI();
1398    return new BoxResourceIterable<BoxFolderLock.Info>(
1399        api, FOLDER_LOCK_URL_TEMPLATE.buildWithQuery(api.getBaseURL(), queryString), 100) {
1400      @Override
1401      protected BoxFolderLock.Info factory(JsonObject jsonObject) {
1402        BoxFolderLock folderLock = new BoxFolderLock(api, jsonObject.get("id").asString());
1403        return folderLock.new Info(jsonObject);
1404      }
1405    };
1406  }
1407
1408  /** Used to specify what direction to sort and display results. */
1409  public enum SortDirection {
1410    /** ASC for ascending order. */
1411    ASC,
1412
1413    /** DESC for descending order. */
1414    DESC
1415  }
1416
1417  /** Enumerates the possible sync states that a folder can have. */
1418  public enum SyncState {
1419    /** The folder is synced. */
1420    SYNCED("synced"),
1421
1422    /** The folder is not synced. */
1423    NOT_SYNCED("not_synced"),
1424
1425    /** The folder is partially synced. */
1426    PARTIALLY_SYNCED("partially_synced");
1427
1428    private final String jsonValue;
1429
1430    SyncState(String jsonValue) {
1431      this.jsonValue = jsonValue;
1432    }
1433
1434    static SyncState fromJSONValue(String jsonValue) {
1435      return SyncState.valueOf(jsonValue.toUpperCase(java.util.Locale.ROOT));
1436    }
1437
1438    String toJSONValue() {
1439      return this.jsonValue;
1440    }
1441  }
1442
1443  /** Enumerates the possible permissions that a user can have on a folder. */
1444  public enum Permission {
1445    /** The user can download the folder. */
1446    CAN_DOWNLOAD("can_download"),
1447
1448    /** The user can upload to the folder. */
1449    CAN_UPLOAD("can_upload"),
1450
1451    /** The user can rename the folder. */
1452    CAN_RENAME("can_rename"),
1453
1454    /** The user can delete the folder. */
1455    CAN_DELETE("can_delete"),
1456
1457    /** The user can share the folder. */
1458    CAN_SHARE("can_share"),
1459
1460    /** The user can invite collaborators to the folder. */
1461    CAN_INVITE_COLLABORATOR("can_invite_collaborator"),
1462
1463    /** The user can set the access level for shared links to the folder. */
1464    CAN_SET_SHARE_ACCESS("can_set_share_access");
1465
1466    private final String jsonValue;
1467
1468    Permission(String jsonValue) {
1469      this.jsonValue = jsonValue;
1470    }
1471
1472    static Permission fromJSONValue(String jsonValue) {
1473      return Permission.valueOf(jsonValue.toUpperCase(java.util.Locale.ROOT));
1474    }
1475
1476    String toJSONValue() {
1477      return this.jsonValue;
1478    }
1479  }
1480
1481  /** Contains information about a BoxFolder. */
1482  public class Info extends BoxItem.Info {
1483    private BoxUploadEmail uploadEmail;
1484    private boolean hasCollaborations;
1485    private SyncState syncState;
1486    private EnumSet<Permission> permissions;
1487    private boolean canNonOwnersInvite;
1488    private boolean isWatermarked;
1489    private boolean isCollaborationRestrictedToEnterprise;
1490    private boolean isExternallyOwned;
1491    private Map<String, Map<String, Metadata>> metadataMap;
1492    private List<String> allowedSharedLinkAccessLevels;
1493    private List<String> allowedInviteeRoles;
1494    private BoxClassification classification;
1495
1496    private boolean isAccessibleViaSharedLink;
1497    private boolean canNonOwnersViewCollaborators;
1498
1499    /** Constructs an empty Info object. */
1500    public Info() {
1501      super();
1502    }
1503
1504    /**
1505     * Constructs an Info object by parsing information from a JSON string.
1506     *
1507     * @param json the JSON string to parse.
1508     */
1509    public Info(String json) {
1510      super(json);
1511    }
1512
1513    /**
1514     * Constructs an Info object using an already parsed JSON object.
1515     *
1516     * @param jsonObject the parsed JSON object.
1517     */
1518    public Info(JsonObject jsonObject) {
1519      super(jsonObject);
1520    }
1521
1522    /**
1523     * Gets the upload email for the folder.
1524     *
1525     * @return the upload email for the folder.
1526     */
1527    public BoxUploadEmail getUploadEmail() {
1528      return this.uploadEmail;
1529    }
1530
1531    /**
1532     * Sets the upload email for the folder.
1533     *
1534     * @param uploadEmail the upload email for the folder.
1535     */
1536    public void setUploadEmail(BoxUploadEmail uploadEmail) {
1537      if (this.uploadEmail == uploadEmail) {
1538        return;
1539      }
1540
1541      this.removeChildObject("folder_upload_email");
1542      this.uploadEmail = uploadEmail;
1543
1544      if (uploadEmail == null) {
1545        this.addPendingChange("folder_upload_email", (String) null);
1546      } else {
1547        this.addChildObject("folder_upload_email", uploadEmail);
1548      }
1549    }
1550
1551    /**
1552     * Gets whether or not the folder has any collaborations.
1553     *
1554     * @return true if the folder has collaborations; otherwise false.
1555     */
1556    public boolean getHasCollaborations() {
1557      return this.hasCollaborations;
1558    }
1559
1560    /**
1561     * Gets the sync state of the folder.
1562     *
1563     * @return the sync state of the folder.
1564     */
1565    public SyncState getSyncState() {
1566      return this.syncState;
1567    }
1568
1569    /**
1570     * Sets the sync state of the folder.
1571     *
1572     * @param syncState the sync state of the folder.
1573     */
1574    public void setSyncState(SyncState syncState) {
1575      this.syncState = syncState;
1576      this.addPendingChange("sync_state", syncState.toJSONValue());
1577    }
1578
1579    /**
1580     * Gets the permissions that the current user has on the folder.
1581     *
1582     * @return the permissions that the current user has on the folder.
1583     */
1584    public EnumSet<Permission> getPermissions() {
1585      return this.permissions;
1586    }
1587
1588    /**
1589     * Gets whether or not the non-owners can invite collaborators to the folder.
1590     *
1591     * @return [description]
1592     */
1593    public boolean getCanNonOwnersInvite() {
1594      return this.canNonOwnersInvite;
1595    }
1596
1597    /**
1598     * Sets whether or not non-owners can invite collaborators to the folder.
1599     *
1600     * @param canNonOwnersInvite indicates non-owners can invite collaborators to the folder.
1601     */
1602    public void setCanNonOwnersInvite(boolean canNonOwnersInvite) {
1603      this.canNonOwnersInvite = canNonOwnersInvite;
1604      this.addPendingChange("can_non_owners_invite", canNonOwnersInvite);
1605    }
1606
1607    /**
1608     * Gets whether future collaborations should be restricted to within the enterprise only.
1609     *
1610     * @return indicates whether collaboration is restricted to enterprise only.
1611     */
1612    public boolean getIsCollaborationRestrictedToEnterprise() {
1613      return this.isCollaborationRestrictedToEnterprise;
1614    }
1615
1616    /**
1617     * Sets whether future collaborations should be restricted to within the enterprise only.
1618     *
1619     * @param isRestricted indicates whether there is collaboration restriction within enterprise.
1620     */
1621    public void setIsCollaborationRestrictedToEnterprise(boolean isRestricted) {
1622      this.isCollaborationRestrictedToEnterprise = isRestricted;
1623      this.addPendingChange("is_collaboration_restricted_to_enterprise", isRestricted);
1624    }
1625
1626    /**
1627     * Retrieves the allowed roles for collaborations.
1628     *
1629     * @return the roles allowed for collaboration.
1630     */
1631    public List<String> getAllowedInviteeRoles() {
1632      return this.allowedInviteeRoles;
1633    }
1634
1635    /**
1636     * Retrieves the allowed access levels for a shared link.
1637     *
1638     * @return the allowed access levels for a shared link.
1639     */
1640    public List<String> getAllowedSharedLinkAccessLevels() {
1641      return this.allowedSharedLinkAccessLevels;
1642    }
1643
1644    /**
1645     * Gets flag indicating whether this file is Watermarked.
1646     *
1647     * @return whether the file is watermarked or not
1648     */
1649    public boolean getIsWatermarked() {
1650      return this.isWatermarked;
1651    }
1652
1653    /**
1654     * Gets the metadata on this folder associated with a specified scope and template. Makes an
1655     * attempt to get metadata that was retrieved using getInfo(String ...) method.
1656     *
1657     * @param templateName the metadata template type name.
1658     * @param scope the scope of the template (usually "global" or "enterprise").
1659     * @return the metadata returned from the server.
1660     */
1661    public Metadata getMetadata(String templateName, String scope) {
1662      try {
1663        return this.metadataMap.get(scope).get(templateName);
1664      } catch (NullPointerException e) {
1665        return null;
1666      }
1667    }
1668
1669    /**
1670     * Get the field is_externally_owned determining whether this folder is owned by a user outside
1671     * of the enterprise.
1672     *
1673     * @return a boolean indicating whether this folder is owned by a user outside the enterprise.
1674     */
1675    public boolean getIsExternallyOwned() {
1676      return this.isExternallyOwned;
1677    }
1678
1679    /**
1680     * Gets the metadata classification type of this folder.
1681     *
1682     * @return the metadata classification type of this folder.
1683     */
1684    public BoxClassification getClassification() {
1685      return this.classification;
1686    }
1687
1688    /**
1689     * Returns the flag indicating whether the folder is accessible via a shared link.
1690     *
1691     * @return boolean flag indicating whether the folder is accessible via a shared link.
1692     */
1693    public boolean getIsAccessibleViaSharedLink() {
1694      return this.isAccessibleViaSharedLink;
1695    }
1696
1697    /**
1698     * Returns the flag indicating if collaborators who are not owners of this folder are restricted
1699     * from viewing other collaborations on this folder.
1700     *
1701     * @return boolean flag indicating if collaborators who are not owners of this folder are
1702     *     restricted from viewing other collaborations on this folder.
1703     */
1704    public boolean getCanNonOwnersViewCollaborators() {
1705      return this.canNonOwnersViewCollaborators;
1706    }
1707
1708    /**
1709     * Sets whether collaborators who are not owners of this folder are restricted from viewing
1710     * other collaborations on this folder.
1711     *
1712     * @param canNonOwnersViewCollaborators indicates if collaborators who are not owners of this
1713     *     folder are restricted from viewing other collaborations on this folderr.
1714     */
1715    public void setCanNonOwnersViewCollaborators(boolean canNonOwnersViewCollaborators) {
1716      this.canNonOwnersViewCollaborators = canNonOwnersViewCollaborators;
1717      this.addPendingChange("can_non_owners_view_collaborators", canNonOwnersViewCollaborators);
1718    }
1719
1720    @Override
1721    public BoxFolder getResource() {
1722      return BoxFolder.this;
1723    }
1724
1725    @Override
1726    protected void parseJSONMember(JsonObject.Member member) {
1727      super.parseJSONMember(member);
1728
1729      String memberName = member.getName();
1730      JsonValue value = member.getValue();
1731      try {
1732        switch (memberName) {
1733          case "folder_upload_email":
1734            if (this.uploadEmail == null) {
1735              this.uploadEmail = new BoxUploadEmail(value.asObject());
1736            } else {
1737              this.uploadEmail.update(value.asObject());
1738            }
1739            break;
1740          case "has_collaborations":
1741            this.hasCollaborations = value.asBoolean();
1742            break;
1743          case "sync_state":
1744            this.syncState = SyncState.fromJSONValue(value.asString());
1745            break;
1746          case "permissions":
1747            this.permissions = this.parsePermissions(value.asObject());
1748            break;
1749          case "can_non_owners_invite":
1750            this.canNonOwnersInvite = value.asBoolean();
1751            break;
1752          case "allowed_shared_link_access_levels":
1753            this.allowedSharedLinkAccessLevels = this.parseSharedLinkAccessLevels(value.asArray());
1754            break;
1755          case "allowed_invitee_roles":
1756            this.allowedInviteeRoles = this.parseAllowedInviteeRoles(value.asArray());
1757            break;
1758          case "is_collaboration_restricted_to_enterprise":
1759            this.isCollaborationRestrictedToEnterprise = value.asBoolean();
1760            break;
1761          case "is_externally_owned":
1762            this.isExternallyOwned = value.asBoolean();
1763            break;
1764          case "watermark_info":
1765            this.isWatermarked = value.asObject().get("is_watermarked").asBoolean();
1766            break;
1767          case "metadata":
1768            this.metadataMap = Parsers.parseAndPopulateMetadataMap(value.asObject());
1769            break;
1770          case "classification":
1771            if (value.isNull()) {
1772              this.classification = null;
1773            } else {
1774              this.classification = new BoxClassification(value.asObject());
1775            }
1776            break;
1777          case "is_accessible_via_shared_link":
1778            this.isAccessibleViaSharedLink = value.asBoolean();
1779            break;
1780          case "can_non_owners_view_collaborators":
1781            this.canNonOwnersViewCollaborators = value.asBoolean();
1782            break;
1783          default:
1784            break;
1785        }
1786      } catch (Exception e) {
1787        throw new BoxDeserializationException(memberName, value.toString(), e);
1788      }
1789    }
1790
1791    private EnumSet<Permission> parsePermissions(JsonObject jsonObject) {
1792      EnumSet<Permission> permissions = EnumSet.noneOf(Permission.class);
1793      for (JsonObject.Member member : jsonObject) {
1794        JsonValue value = member.getValue();
1795        if (value.isNull() || !value.asBoolean()) {
1796          continue;
1797        }
1798
1799        try {
1800          permissions.add(BoxFolder.Permission.fromJSONValue(member.getName()));
1801        } catch (IllegalArgumentException ignored) {
1802          // If the permission is not recognized, we ignore it.
1803        }
1804      }
1805
1806      return permissions;
1807    }
1808
1809    private List<String> parseSharedLinkAccessLevels(JsonArray jsonArray) {
1810      List<String> accessLevels = new ArrayList<>(jsonArray.size());
1811      for (JsonValue value : jsonArray) {
1812        accessLevels.add(value.asString());
1813      }
1814
1815      return accessLevels;
1816    }
1817
1818    private List<String> parseAllowedInviteeRoles(JsonArray jsonArray) {
1819      List<String> roles = new ArrayList<>(jsonArray.size());
1820      for (JsonValue value : jsonArray) {
1821        roles.add(value.asString());
1822      }
1823
1824      return roles;
1825    }
1826  }
1827}