001package com.box.sdk;
002
003import com.eclipsesource.json.Json;
004import com.eclipsesource.json.JsonArray;
005import com.eclipsesource.json.JsonObject;
006import com.eclipsesource.json.JsonValue;
007import java.net.URL;
008import java.util.ArrayList;
009import java.util.Collection;
010import java.util.Date;
011
012/**
013 * Represents a collaboration between a user and another user or group. Collaborations are Box's equivalent of access
014 * control lists. They can be used to set and apply permissions for users or groups to folders.
015 *
016 * <p>Unless otherwise noted, the methods in this class can throw an unchecked {@link BoxAPIException} (unchecked
017 * meaning that the compiler won't force you to handle it) if an error occurs. If you wish to implement custom error
018 * handling for errors related to the Box REST API, you should capture this exception explicitly.</p>
019 */
020@BoxResourceType("collaboration")
021public class BoxCollaboration extends BoxResource {
022
023    /**
024     * All possible fields on a collaboration object.
025     */
026    public static final String[] ALL_FIELDS = {"type", "id", "item", "accessible_by", "role", "expires_at",
027        "can_view_path", "status", "acknowledged_at", "created_by",
028        "created_at", "modified_at"};
029
030    /**
031     * Collaborations URL Template.
032     */
033    public static final URLTemplate COLLABORATIONS_URL_TEMPLATE = new URLTemplate("collaborations");
034    /**
035     * Pending Collaborations URL.
036     */
037    public static final URLTemplate PENDING_COLLABORATIONS_URL = new URLTemplate("collaborations?status=pending");
038    /**
039     * Collaboration URL Template.
040     */
041    public static final URLTemplate COLLABORATION_URL_TEMPLATE = new URLTemplate("collaborations/%s");
042    /**
043     * Get All File Collaboations URL.
044     */
045    public static final URLTemplate GET_ALL_FILE_COLLABORATIONS_URL = new URLTemplate("files/%s/collaborations");
046
047    /**
048     * Constructs a BoxCollaboration for a collaboration with a given ID.
049     *
050     * @param api the API connection to be used by the collaboration.
051     * @param id  the ID of the collaboration.
052     */
053    public BoxCollaboration(BoxAPIConnection api, String id) {
054        super(api, id);
055    }
056
057    /**
058     * Create a new collaboration object.
059     *
060     * @param api          the API connection used to make the request.
061     * @param accessibleBy the JSON object describing who should be collaborated.
062     * @param item         the JSON object describing which item to collaborate.
063     * @param role         the role to give the collaborators.
064     * @param notify       the user/group should receive email notification of the collaboration or not.
065     * @param canViewPath  the view path collaboration feature is enabled or not.
066     * @return info about the new collaboration.
067     */
068    protected static BoxCollaboration.Info create(BoxAPIConnection api, JsonObject accessibleBy, JsonObject item,
069                                                  BoxCollaboration.Role role, Boolean notify, Boolean canViewPath) {
070        return create(api, accessibleBy, item, role, notify, canViewPath, null);
071    }
072
073    /**
074     * Create a new collaboration object.
075     *
076     * @param api          the API connection used to make the request.
077     * @param accessibleBy the JSON object describing who should be collaborated.
078     * @param item         the JSON object describing which item to collaborate.
079     * @param role         the role to give the collaborators.
080     * @param notify       the user/group should receive email notification of the collaboration or not.
081     * @param canViewPath  the view path collaboration feature is enabled or not.
082     * @param expiresAt    the date the collaboration expires
083     * @return info about the new collaboration.
084     */
085    protected static BoxCollaboration.Info create(
086        BoxAPIConnection api,
087        JsonObject accessibleBy,
088        JsonObject item,
089        BoxCollaboration.Role role,
090        Boolean notify,
091        Boolean canViewPath,
092        Date expiresAt
093    ) {
094
095        String queryString = "";
096        if (notify != null) {
097            queryString = new QueryStringBuilder().appendParam("notify", notify.toString()).toString();
098        }
099        URL url;
100        if (queryString.length() > 0) {
101            url = COLLABORATIONS_URL_TEMPLATE.buildWithQuery(api.getBaseURL(), queryString);
102        } else {
103            url = COLLABORATIONS_URL_TEMPLATE.build(api.getBaseURL());
104        }
105
106        JsonObject requestJSON = new JsonObject();
107        requestJSON.add("item", item);
108        requestJSON.add("accessible_by", accessibleBy);
109        requestJSON.add("role", role.toJSONString());
110        if (canViewPath != null) {
111            requestJSON.add("can_view_path", canViewPath);
112        }
113        if (expiresAt != null) {
114            requestJSON.add("expires_at", BoxDateFormat.format(expiresAt));
115        }
116
117        BoxJSONRequest request = new BoxJSONRequest(api, url, "POST");
118
119        request.setBody(requestJSON.toString());
120        try (BoxJSONResponse response = request.send()) {
121            JsonObject responseJSON = Json.parse(response.getJSON()).asObject();
122            BoxCollaboration newCollaboration = new BoxCollaboration(api, responseJSON.get("id").asString());
123            return newCollaboration.new Info(responseJSON);
124        }
125    }
126
127    /**
128     * Gets all pending collaboration invites for the current user.
129     *
130     * @param api the API connection to use.
131     * @return a collection of pending collaboration infos.
132     */
133    public static Collection<Info> getPendingCollaborations(BoxAPIConnection api, String... fields) {
134        QueryStringBuilder queryBuilder = new QueryStringBuilder();
135        queryBuilder.appendParam("status", "pending");
136        if (fields.length > 0) {
137            queryBuilder.appendParam("fields", fields);
138        }
139        URL url = COLLABORATIONS_URL_TEMPLATE.buildWithQuery(api.getBaseURL(), queryBuilder.toString());
140
141
142        BoxJSONRequest request = new BoxJSONRequest(api, url, "GET");
143        try (BoxJSONResponse response = request.send()) {
144            JsonObject responseJSON = Json.parse(response.getJSON()).asObject();
145
146            int entriesCount = responseJSON.get("total_count").asInt();
147            Collection<BoxCollaboration.Info> collaborations = new ArrayList<>(entriesCount);
148            JsonArray entries = responseJSON.get("entries").asArray();
149            for (JsonValue entry : entries) {
150                JsonObject entryObject = entry.asObject();
151                BoxCollaboration collaboration = new BoxCollaboration(api, entryObject.get("id").asString());
152                BoxCollaboration.Info info = collaboration.new Info(entryObject);
153                collaborations.add(info);
154            }
155
156            return collaborations;
157        }
158    }
159
160    /**
161     * Used to retrieve all collaborations associated with the item.
162     *
163     * @param api      BoxAPIConnection from the associated file.
164     * @param fileID   FileID of the associated file
165     * @param pageSize page size for server pages of the Iterable
166     * @param fields   the optional fields to retrieve.
167     * @return An iterable of BoxCollaboration.Info instances associated with the item.
168     */
169    public static BoxResourceIterable<Info> getAllFileCollaborations(final BoxAPIConnection api, String fileID,
170                                                                     int pageSize, String... fields) {
171        QueryStringBuilder builder = new QueryStringBuilder();
172        if (fields.length > 0) {
173            builder.appendParam("fields", fields);
174        }
175        return new BoxResourceIterable<BoxCollaboration.Info>(
176            api, GET_ALL_FILE_COLLABORATIONS_URL.buildWithQuery(api.getBaseURL(), builder.toString(), fileID),
177            pageSize) {
178
179            @Override
180            protected BoxCollaboration.Info factory(JsonObject jsonObject) {
181                String id = jsonObject.get("id").asString();
182                return new BoxCollaboration(api, id).new Info(jsonObject);
183            }
184        };
185    }
186
187    /**
188     * Gets information about this collection with a custom set of fields.
189     *
190     * @param fields the fields to retrieve.
191     * @return info about the collaboration.
192     */
193    public Info getInfo(String... fields) {
194        URL url = COLLABORATION_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID());
195        if (fields.length > 0) {
196            String queryString = new QueryStringBuilder().appendParam("fields", fields).toString();
197            url = COLLABORATION_URL_TEMPLATE.buildWithQuery(this.getAPI().getBaseURL(), queryString, this.getID());
198        }
199
200        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "GET");
201        try (BoxJSONResponse response = request.send()) {
202            return new Info(response.getJSON());
203        }
204    }
205
206    /**
207     * Updates the information about this collaboration with any info fields that have been modified locally.
208     *
209     * @param info the updated info.
210     */
211    public void updateInfo(Info info) {
212        BoxAPIConnection api = this.getAPI();
213        URL url = COLLABORATION_URL_TEMPLATE.build(api.getBaseURL(), this.getID());
214
215        BoxJSONRequest request = new BoxJSONRequest(api, url, "PUT");
216        request.setBody(info.getPendingChanges());
217        try (BoxJSONResponse response = request.send()) {
218            JsonObject jsonObject = Json.parse(response.getJSON()).asObject();
219            info.update(jsonObject);
220        }
221    }
222
223    /**
224     * Deletes this collaboration.
225     */
226    public void delete() {
227        BoxAPIConnection api = this.getAPI();
228        URL url = COLLABORATION_URL_TEMPLATE.build(api.getBaseURL(), this.getID());
229
230        BoxAPIRequest request = new BoxAPIRequest(api, url, "DELETE");
231        request.send().close();
232    }
233
234    /**
235     * Enumerates the possible statuses that a collaboration can have.
236     */
237    public enum Status {
238        /**
239         * The collaboration has been accepted.
240         */
241        ACCEPTED,
242
243        /**
244         * The collaboration is waiting to be accepted or rejected.
245         */
246        PENDING,
247
248        /**
249         * The collaboration has been rejected.
250         */
251        REJECTED
252    }
253
254    /**
255     * Enumerates the possible access levels that a collaborator can have.
256     */
257    public enum Role {
258        /**
259         * An Editor has full read/write access to a folder. Once invited to a folder, they will be able to view,
260         * download, upload, edit, delete, copy, move, rename, generate shared links, make comments, assign tasks,
261         * create tags, and invite/remove collaborators. They will not be able to delete or move root level folders.
262         */
263        EDITOR("editor"),
264
265        /**
266         * The viewer role has full read access to a folder. Once invited to a folder, they will be able to preview,
267         * download, make comments, and generate shared links.  They will not be able to add tags, invite new
268         * collaborators, upload, edit, or delete items in the folder.
269         */
270        VIEWER("viewer"),
271
272        /**
273         * The previewer role has limited read access to a folder. They will only be able to preview the items in the
274         * folder using the integrated content viewer. They will not be able to share, upload, edit, or delete any
275         * content. This role is only available to enterprise accounts.
276         */
277        PREVIEWER("previewer"),
278
279        /**
280         * The uploader has limited write access to a folder. They will only be able to upload and see the names of the
281         * items in a folder. They will not able to download or view any content. This role is only available to
282         * enterprise accounts.
283         */
284        UPLOADER("uploader"),
285
286        /**
287         * The previewer-uploader role is a combination of previewer and uploader. A user with this access level will be
288         * able to preview files using the integrated content viewer as well as upload items into the folder. They will
289         * not be able to download, edit, or share, items in the folder. This role is only available to enterprise
290         * accounts.
291         */
292        PREVIEWER_UPLOADER("previewer uploader"),
293
294        /**
295         * The viewer-uploader role is a combination of viewer and uploader. A viewer-uploader has full read access to a
296         * folder and limited write access. They are able to preview, download, add comments, generate shared links, and
297         * upload content to the folder. They will not be able to add tags, invite new collaborators, edit, or delete
298         * items in the folder. This role is only available to enterprise accounts.
299         */
300        VIEWER_UPLOADER("viewer uploader"),
301
302        /**
303         * The co-owner role has all of the functional read/write access that an editor does. This permission level has
304         * the added ability of being able to manage users in the folder. A co-owner can add new collaborators, change
305         * access levels of existing collaborators, and remove collaborators. However, they will not be able to
306         * manipulate the owner of the folder or transfer ownership to another user. This role is only available to
307         * enterprise accounts.
308         */
309        CO_OWNER("co-owner"),
310
311        /**
312         * The owner role has all of the functional capabilities of a co-owner. However, they will be able to manipulate
313         * the owner of the folder or transfer ownership to another user. This role is only available to enterprise
314         * accounts.
315         */
316        OWNER("owner");
317
318        private final String jsonValue;
319
320        Role(String jsonValue) {
321            this.jsonValue = jsonValue;
322        }
323
324        static Role fromJSONString(String jsonValue) {
325            switch (jsonValue) {
326                case "editor":
327                    return EDITOR;
328                case "viewer":
329                    return VIEWER;
330                case "previewer":
331                    return PREVIEWER;
332                case "uploader":
333                    return UPLOADER;
334                case "previewer uploader":
335                    return PREVIEWER_UPLOADER;
336                case "viewer uploader":
337                    return VIEWER_UPLOADER;
338                case "co-owner":
339                    return CO_OWNER;
340                case "owner":
341                    return OWNER;
342                default:
343                    throw new IllegalArgumentException("The provided JSON value isn't a valid Role.");
344            }
345        }
346
347        String toJSONString() {
348            return this.jsonValue;
349        }
350    }
351
352    /**
353     * Contains information about a BoxCollaboration.
354     */
355    public class Info extends BoxResource.Info {
356        private BoxUser.Info createdBy;
357        private Date createdAt;
358        private Date modifiedAt;
359        private Date expiresAt;
360        private Status status;
361        private BoxCollaborator.Info accessibleBy;
362        private Role role;
363        private Date acknowledgedAt;
364        private BoxItem.Info item;
365        private String inviteEmail;
366        private boolean canViewPath;
367
368        /**
369         * Constructs an empty Info object.
370         */
371        public Info() {
372            super();
373        }
374
375        /**
376         * Constructs an Info object by parsing information from a JSON string.
377         *
378         * @param json the JSON string to parse.
379         */
380        public Info(String json) {
381            super(json);
382        }
383
384        Info(JsonObject jsonObject) {
385            super(jsonObject);
386        }
387
388        /**
389         * Gets the user who created the collaboration.
390         *
391         * @return the user who created the collaboration.
392         */
393        public BoxUser.Info getCreatedBy() {
394            return this.createdBy;
395        }
396
397        /**
398         * Gets the time the collaboration was created.
399         *
400         * @return the time the collaboration was created.
401         */
402        public Date getCreatedAt() {
403            return this.createdAt;
404        }
405
406        /**
407         * Gets the time the collaboration was last modified.
408         *
409         * @return the time the collaboration was last modified.
410         */
411        public Date getModifiedAt() {
412            return this.modifiedAt;
413        }
414
415        /**
416         * Gets the time the collaboration will expire.
417         *
418         * @return the time the collaboration will expire.
419         */
420        public Date getExpiresAt() {
421            return this.expiresAt;
422        }
423
424        /**
425         * Set the time the collaboration will expire.
426         *
427         * @param expiresAt the expiration date of the collaboration.
428         */
429        public void setExpiresAt(Date expiresAt) {
430            this.expiresAt = expiresAt;
431            this.addPendingChange("expires_at", BoxDateFormat.format(expiresAt));
432        }
433
434        /**
435         * Gets a boolean indicator whether "view path collaboration" feature is enabled or not.
436         * When set to true this allows the invitee to see the entire parent path to the item.
437         * It is important to note that this does not grant privileges in any parent folder.
438         *
439         * @return the Boolean value indicating if "view path collaboration" is enabled or not
440         */
441        public boolean getCanViewPath() {
442            return this.canViewPath;
443        }
444
445        /**
446         * Sets the permission for "view path collaboration" feature. When set to true this allows
447         * the invitee to to see the entire parent path to the item
448         *
449         * @param canViewState the boolean value indicating whether the invitee can see the parent folder.
450         */
451        public void setCanViewPath(boolean canViewState) {
452            this.canViewPath = canViewState;
453            this.addPendingChange("can_view_path", canViewState);
454        }
455
456        /**
457         * The email address used to invite an un-registered collaborator, if they are not a registered user.
458         *
459         * @return the email for the un-registed collaborator.
460         */
461        public String getInviteEmail() {
462            return this.inviteEmail;
463        }
464
465        /**
466         * Gets the status of the collaboration.
467         *
468         * @return the status of the collaboration.
469         */
470        public Status getStatus() {
471            return this.status;
472        }
473
474        /**
475         * Sets the status of the collaboration in order to accept or reject the collaboration if it's pending.
476         *
477         * @param status the new status of the collaboration.
478         */
479        public void setStatus(Status status) {
480            this.status = status;
481            this.addPendingChange("status", status.name().toLowerCase());
482        }
483
484        /**
485         * Gets the collaborator who this collaboration applies to.
486         *
487         * @return the collaborator who this collaboration applies to.
488         */
489        public BoxCollaborator.Info getAccessibleBy() {
490            return this.accessibleBy;
491        }
492
493        /**
494         * Gets the level of access the collaborator has.
495         *
496         * @return the level of access the collaborator has.
497         */
498        public Role getRole() {
499            return this.role;
500        }
501
502        /**
503         * Sets the level of access the collaborator has.
504         *
505         * @param role the new level of access to give the collaborator.
506         */
507        public void setRole(Role role) {
508            this.role = role;
509            this.addPendingChange("role", role.toJSONString());
510        }
511
512        /**
513         * Gets the time the collaboration's status was changed.
514         *
515         * @return the time the collaboration's status was changed.
516         */
517        public Date getAcknowledgedAt() {
518            return this.acknowledgedAt;
519        }
520
521        /**
522         * Gets the folder the collaboration is related to.
523         *
524         * @return the folder the collaboration is related to.
525         */
526        public BoxItem.Info getItem() {
527            return this.item;
528        }
529
530        @Override
531        public BoxCollaboration getResource() {
532            return BoxCollaboration.this;
533        }
534
535        @SuppressWarnings("checkstyle:MissingSwitchDefault")
536        @Override
537        protected void parseJSONMember(JsonObject.Member member) {
538            super.parseJSONMember(member);
539
540            String memberName = member.getName();
541            JsonValue value = member.getValue();
542            try {
543                switch (memberName) {
544                    case "created_by":
545                        JsonObject userJSON = value.asObject();
546                        if (this.createdBy == null) {
547                            String userID = userJSON.get("id").asString();
548                            BoxUser user = new BoxUser(getAPI(), userID);
549                            this.createdBy = user.new Info(userJSON);
550                        } else {
551                            this.createdBy.update(userJSON);
552                        }
553                        break;
554                    case "created_at":
555                        this.createdAt = BoxDateFormat.parse(value.asString());
556
557                        break;
558                    case "modified_at":
559                        this.modifiedAt = BoxDateFormat.parse(value.asString());
560                        break;
561                    case "expires_at":
562                        this.expiresAt = BoxDateFormat.parse(value.asString());
563                        break;
564                    case "status":
565                        String statusString = value.asString().toUpperCase();
566                        this.status = Status.valueOf(statusString);
567
568                        break;
569                    case "accessible_by":
570                        JsonObject accessibleByJSON = value.asObject();
571                        if (this.accessibleBy == null) {
572                            this.accessibleBy = this.parseAccessibleBy(accessibleByJSON);
573                        } else {
574                            this.updateAccessibleBy(accessibleByJSON);
575                        }
576                        break;
577                    case "role":
578                        this.role = Role.fromJSONString(value.asString());
579                        break;
580                    case "acknowledged_at":
581                        this.acknowledgedAt = BoxDateFormat.parse(value.asString());
582                        break;
583                    case "can_view_path":
584                        this.canViewPath = value.asBoolean();
585                        break;
586                    case "invite_email":
587                        this.inviteEmail = value.asString();
588                        break;
589                    case "item":
590                        JsonObject itemJson = value.asObject();
591                        if (this.item == null) {
592                            this.item = selectCollaborationItem(itemJson);
593                        } else {
594                            this.item.update(itemJson);
595                        }
596                        break;
597                }
598            } catch (Exception e) {
599                throw new BoxDeserializationException(memberName, value.toString(), e);
600            }
601        }
602
603        private BoxItem.Info selectCollaborationItem(JsonObject itemJson) {
604            String itemId = itemJson.get("id").asString();
605            String itemType = itemJson.get("type").asString();
606            switch (itemType) {
607                case BoxFile.TYPE:
608                    return new BoxFile(getAPI(), itemId).new Info(itemJson);
609                case BoxFolder.TYPE:
610                    return new BoxFolder(getAPI(), itemId).new Info(itemJson);
611                default:
612                    throw new IllegalStateException(
613                        String.format(
614                            "Unsupported collaboration item type '%s': JSON %n%s",
615                            itemType,
616                            itemJson
617                        ));
618            }
619        }
620
621        private void updateAccessibleBy(JsonObject json) {
622            String type = json.get("type").asString();
623            if ((type.equals("user") && this.accessibleBy instanceof BoxUser.Info)
624                || (type.equals("group") && this.accessibleBy instanceof BoxGroup.Info)) {
625
626                this.accessibleBy.update(json);
627            } else {
628                this.accessibleBy = this.parseAccessibleBy(json);
629            }
630        }
631
632        private BoxCollaborator.Info parseAccessibleBy(JsonObject json) {
633            String id = json.get("id").asString();
634            String type = json.get("type").asString();
635            BoxCollaborator.Info parsedInfo = null;
636            if (type.equals("user")) {
637                BoxUser user = new BoxUser(getAPI(), id);
638                parsedInfo = user.new Info(json);
639            } else if (type.equals("group")) {
640                BoxGroup group = new BoxGroup(getAPI(), id);
641                parsedInfo = group.new Info(json);
642            }
643
644            return parsedInfo;
645        }
646    }
647}