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