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