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}