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