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