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