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.Date; 010import java.util.List; 011 012/** 013 * Represents a Sign Request used by Box Sign. 014 * Sign Requests are used to request e-signatures on documents from signers. 015 * A Sign Request can refer to one or more Box Files and can be sent to one or more Box Sign Request Signers. 016 * 017 * @see <a href="https://developer.box.com/reference/resources/sign-requests/">Box Sign Request</a> 018 * 019 * <p>Unless otherwise noted, the methods in this class can throw an unchecked {@link BoxAPIException} (unchecked 020 * meaning that the compiler won't force you to handle it) if an error occurs. If you wish to implement custom error 021 * handling for errors related to the Box REST API, you should capture this exception explicitly.</p> 022 */ 023@BoxResourceType("sign_request") 024public class BoxSignRequest extends BoxResource { 025 026 /** 027 * The URL template used for operation Sign Request operations. 028 */ 029 public static final URLTemplate SIGN_REQUESTS_URL_TEMPLATE = new URLTemplate("sign_requests"); 030 031 /** 032 * The URL template used for Sign Request operations with a given ID. 033 */ 034 public static final URLTemplate SIGN_REQUEST_URL_TEMPLATE = new URLTemplate("sign_requests/%s"); 035 036 /** 037 * The URL template used to cancel an existing Sign Request. 038 */ 039 public static final URLTemplate SIGN_REQUEST_CANCEL_URL_TEMPLATE = new URLTemplate("sign_requests/%s/cancel"); 040 041 /** 042 * The URL template used to resend an existing Sign Request. 043 */ 044 public static final URLTemplate SIGN_REQUEST_RESEND_URL_TEMPLATE = new URLTemplate("sign_requests/%s/resend"); 045 046 /** 047 * The default limit of entries per response. 048 */ 049 private static final int DEFAULT_LIMIT = 100; 050 051 /** 052 * Constructs a BoxResource for a resource with a given ID. 053 * 054 * @param api the API connection to be used by the resource. 055 * @param id the ID of the resource. 056 */ 057 public BoxSignRequest(BoxAPIConnection api, String id) { 058 super(api, id); 059 } 060 061 /** 062 * Used to create a new sign request using existing BoxFile.Info models. 063 * 064 * @param api the API connection to be used by the created user. 065 * @param sourceFiles the list of BoxFile.Info files to create a signing document from. 066 * @param signers the list of signers for this sign request. 067 * @param parentFolderId the id of the destination folder to place sign request specific data in. 068 * @param optionalParams the optional parameters. 069 * @return the created sign request's info. 070 */ 071 public static BoxSignRequest.Info createSignRequestFromFiles(BoxAPIConnection api, 072 List<BoxFile.Info> sourceFiles, 073 List<BoxSignRequestSigner> signers, 074 String parentFolderId, 075 BoxSignRequestCreateParams optionalParams) { 076 return createSignRequest(api, toBoxSignRequestFiles(sourceFiles), signers, parentFolderId, optionalParams); 077 } 078 079 /** 080 * Used to create a new sign request using BoxFile.Info models. 081 * 082 * @param api the API connection to be used by the created user. 083 * @param sourceFiles the list of BoxFile.Info files to create a signing document from. 084 * @param signers the list of signers for this sign request. 085 * @param parentFolderId the id of the destination folder to place sign request specific data in. 086 * @return the created sign request's info. 087 */ 088 public static BoxSignRequest.Info createSignRequestFromFiles(BoxAPIConnection api, 089 List<BoxFile.Info> sourceFiles, 090 List<BoxSignRequestSigner> signers, 091 String parentFolderId) { 092 093 return createSignRequest(api, toBoxSignRequestFiles(sourceFiles), signers, parentFolderId, null); 094 } 095 096 /** 097 * Used to create a new sign request. 098 * 099 * @param api the API connection to be used by the created user. 100 * @param sourceFiles the list of files to a signing document from. 101 * @param signers the list of signers for this sign request. 102 * @param parentFolderId the id of the destination folder to place sign request specific data in. 103 * @return the created sign request's info. 104 */ 105 public static BoxSignRequest.Info createSignRequest(BoxAPIConnection api, List<BoxSignRequestFile> sourceFiles, 106 List<BoxSignRequestSigner> signers, String parentFolderId) { 107 return createSignRequest(api, sourceFiles, signers, parentFolderId, null); 108 } 109 110 /** 111 * Used to create a new sign request with optional parameters. 112 * 113 * @param api the API connection to be used by the created user. 114 * @param signers the list of signers for this sign request. 115 * @param sourceFiles the list of files to a signing document from. 116 * @param parentFolderId the id of the destination folder to place sign request specific data in. 117 * @param optionalParams the optional parameters. 118 * @return the created sign request's info. 119 */ 120 public static BoxSignRequest.Info createSignRequest(BoxAPIConnection api, List<BoxSignRequestFile> sourceFiles, 121 List<BoxSignRequestSigner> signers, String parentFolderId, 122 BoxSignRequestCreateParams optionalParams) { 123 124 JsonObject requestJSON = new JsonObject(); 125 126 JsonArray sourceFilesJSON = new JsonArray(); 127 for (BoxSignRequestFile sourceFile : sourceFiles) { 128 sourceFilesJSON.add(sourceFile.getJSONObject()); 129 } 130 requestJSON.add("source_files", sourceFilesJSON); 131 132 JsonArray signersJSON = new JsonArray(); 133 for (BoxSignRequestSigner signer : signers) { 134 signersJSON.add(signer.getJSONObject()); 135 } 136 requestJSON.add("signers", signersJSON); 137 138 JsonObject parentFolderJSON = new JsonObject(); 139 parentFolderJSON.add("id", parentFolderId); 140 parentFolderJSON.add("type", "folder"); 141 requestJSON.add("parent_folder", parentFolderJSON); 142 143 if (optionalParams != null) { 144 optionalParams.appendParamsAsJson(requestJSON); 145 } 146 147 URL url = SIGN_REQUESTS_URL_TEMPLATE.build(api.getBaseURL()); 148 BoxJSONRequest request = new BoxJSONRequest(api, url, "POST"); 149 request.setBody(requestJSON.toString()); 150 BoxJSONResponse response = (BoxJSONResponse) request.send(); 151 JsonObject responseJSON = Json.parse(response.getJSON()).asObject(); 152 BoxSignRequest signRequest = new BoxSignRequest(api, responseJSON.get("id").asString()); 153 return signRequest.new Info(responseJSON); 154 } 155 156 /** 157 * Returns all the sign requests. 158 * 159 * @param api the API connection to be used by the resource. 160 * @param fields the fields to retrieve. 161 * @return an iterable with all the sign requests. 162 */ 163 public static Iterable<BoxSignRequest.Info> getAll(final BoxAPIConnection api, String... fields) { 164 return getAll(api, DEFAULT_LIMIT, fields); 165 } 166 167 /** 168 * Returns all the sign requests. 169 * 170 * @param api the API connection to be used by the resource. 171 * @param limit the limit of items per single response. The default value is 100. 172 * @param fields the fields to retrieve. 173 * @return an iterable with all the sign requests. 174 */ 175 public static Iterable<BoxSignRequest.Info> getAll(final BoxAPIConnection api, int limit, String... fields) { 176 QueryStringBuilder queryString = new QueryStringBuilder(); 177 if (fields.length > 0) { 178 queryString.appendParam("fields", fields); 179 } 180 URL url = SIGN_REQUESTS_URL_TEMPLATE.buildWithQuery(api.getBaseURL(), queryString.toString()); 181 return new BoxResourceIterable<BoxSignRequest.Info>(api, url, limit) { 182 183 @Override 184 protected BoxSignRequest.Info factory(JsonObject jsonObject) { 185 BoxSignRequest signRequest = new BoxSignRequest(api, jsonObject.get("id").asString()); 186 return signRequest.new Info(jsonObject); 187 } 188 189 }; 190 } 191 192 private static List<BoxSignRequestFile> toBoxSignRequestFiles(List<BoxFile.Info> sourceFiles) { 193 List<BoxSignRequestFile> files = new ArrayList<>(); 194 for (BoxFile.Info sourceFile : sourceFiles) { 195 BoxSignRequestFile file = BoxSignRequestFile.fromFile(sourceFile); 196 files.add(file); 197 } 198 199 return files; 200 } 201 202 /** 203 * Returns information about this sign request. 204 * 205 * @param fields the fields to retrieve. 206 * @return information about this sign request. 207 */ 208 public BoxSignRequest.Info getInfo(String... fields) { 209 QueryStringBuilder builder = new QueryStringBuilder(); 210 if (fields.length > 0) { 211 builder.appendParam("fields", fields); 212 } 213 URL url = SIGN_REQUEST_URL_TEMPLATE.buildAlphaWithQuery( 214 this.getAPI().getBaseURL(), builder.toString(), this.getID()); 215 BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET"); 216 BoxJSONResponse response = (BoxJSONResponse) request.send(); 217 JsonObject responseJSON = Json.parse(response.getJSON()).asObject(); 218 return new BoxSignRequest.Info(responseJSON); 219 } 220 221 /** 222 * Cancels a sign request if it has not yet been signed or declined. 223 * Any outstanding signers will no longer be able to sign the document. 224 * 225 * @return the cancelled sign request's info. 226 */ 227 public BoxSignRequest.Info cancel() { 228 URL url = SIGN_REQUEST_CANCEL_URL_TEMPLATE.buildAlphaWithQuery(getAPI().getBaseURL(), "", this.getID()); 229 BoxAPIRequest request = new BoxAPIRequest(getAPI(), url, "POST"); 230 request.noBody(); 231 BoxJSONResponse response = (BoxJSONResponse) request.send(); 232 JsonObject responseJSON = Json.parse(response.getJSON()).asObject(); 233 return new BoxSignRequest.Info(responseJSON); 234 } 235 236 /** 237 * Attempts to resend a Sign Request to all signers that have not signed yet. 238 * There is a 10 minute cooling-off period between each resend request. 239 * If you make a resend call during the cooling-off period, a BoxAPIException will be thrown. 240 */ 241 public void resend() { 242 URL url = SIGN_REQUEST_RESEND_URL_TEMPLATE.buildAlphaWithQuery(getAPI().getBaseURL(), "", this.getID()); 243 BoxJSONRequest request = new BoxJSONRequest(getAPI(), url, "POST"); 244 BoxAPIResponse response = request.send(); 245 response.disconnect(); 246 } 247 248 /** 249 * Represents a status of the sign request. 250 */ 251 public enum BoxSignRequestStatus { 252 253 /** 254 * Converting status. 255 */ 256 Converting("converting"), 257 258 /** 259 * Created status. 260 */ 261 Created("created"), 262 263 /** 264 * Sent status. 265 */ 266 Sent("sent"), 267 268 /** 269 * Viewed status. 270 */ 271 Viewed("viewed"), 272 273 /** 274 * Signed status. 275 */ 276 Signed("signed"), 277 278 /** 279 * Cancelled status. 280 */ 281 Cancelled("cancelled"), 282 283 /** 284 * Declined status. 285 */ 286 Declined("declined"), 287 288 /** 289 * Error converting status. 290 */ 291 ErrorConverting("error_converting"), 292 293 /** 294 * Error sending status. 295 */ 296 ErrorSending("error_sending"), 297 298 /** 299 * Expired status. 300 */ 301 Expired("expired"); 302 303 private final String jsonValue; 304 305 BoxSignRequestStatus(String jsonValue) { 306 this.jsonValue = jsonValue; 307 } 308 309 static BoxSignRequestStatus fromJSONString(String jsonValue) { 310 if ("converting".equals(jsonValue)) { 311 return Converting; 312 } else if ("created".equals(jsonValue)) { 313 return Created; 314 } else if ("sent".equals(jsonValue)) { 315 return Sent; 316 } else if ("viewed".equals(jsonValue)) { 317 return Viewed; 318 } else if ("signed".equals(jsonValue)) { 319 return Signed; 320 } else if ("cancelled".equals(jsonValue)) { 321 return Cancelled; 322 } else if ("declined".equals(jsonValue)) { 323 return Declined; 324 } else if ("error_converting".equals(jsonValue)) { 325 return ErrorConverting; 326 } else if ("error_sending".equals(jsonValue)) { 327 return ErrorSending; 328 } else if ("expired".equals(jsonValue)) { 329 return Expired; 330 } 331 throw new IllegalArgumentException("The provided JSON value isn't a valid BoxSignRequestStatus value."); 332 } 333 } 334 335 /** 336 * Contains information about the Sign Request. 337 */ 338 public class Info extends BoxResource.Info { 339 340 private boolean isDocumentPreparationNeeded; 341 private boolean areTextSignaturesEnabled; 342 private boolean areDatesEnabled; 343 private BoxSignRequestSignatureColor signatureColor; 344 private String emailSubject; 345 private String emailMessage; 346 private boolean areRemindersEnabled; 347 private List<BoxFile.Info> sourceFiles; 348 private BoxFolder.Info parentFolder; 349 private List<BoxSignRequestSigner> signers; 350 private String name; 351 private List<BoxSignRequestPrefillTag> prefillTags; 352 private Integer daysValid; 353 private String externalId; 354 private String prepareUrl; 355 private BoxFile.Info signingLog; 356 private BoxSignRequestStatus status; 357 private BoxSignRequestSignFiles signFiles; 358 private Date autoExpireAt; 359 private String redirectUrl; 360 private String declinedRedirectUrl; 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 /** 379 * Constructs an Info object using an already parsed JSON object. 380 * 381 * @param jsonObject the parsed JSON object. 382 */ 383 Info(JsonObject jsonObject) { 384 super(jsonObject); 385 } 386 387 /** 388 * Indicates if the sender should receive a prepare_url in the response to complete document preparation via UI. 389 * 390 * @return true if document preparation is needed, otherwise false. 391 */ 392 public boolean getIsDocumentPreparationNeeded() { 393 return this.isDocumentPreparationNeeded; 394 } 395 396 /** 397 * Gets the flag indicating if usage of signatures generated by typing (text) is enabled. 398 * 399 * @return true if text signatures are enabled, otherwise false. 400 */ 401 public boolean getAreTextSignaturesEnabled() { 402 return this.areTextSignaturesEnabled; 403 } 404 405 /** 406 * Gets the flag indicating if ability for signer to add dates is enabled. 407 * 408 * @return true if ability for signer to add dates is enabled, otherwise false. 409 */ 410 public boolean getAreDatesEnabled() { 411 return this.areDatesEnabled; 412 } 413 414 /** 415 * Gets the forced, specific color for the signature. 416 * 417 * @return signature color (blue, black, red). 418 */ 419 public BoxSignRequestSignatureColor getSignatureColor() { 420 return this.signatureColor; 421 } 422 423 /** 424 * Gets the subject of the sign request email. 425 * 426 * @return subject of the sign request email. 427 */ 428 public String getEmailSubject() { 429 return this.emailSubject; 430 } 431 432 /** 433 * Gets the message to include in the sign request email. 434 * 435 * @return message of sign request email. 436 */ 437 public String getEmailMessage() { 438 return this.emailMessage; 439 } 440 441 /** 442 * Gets the flag indicating if sending reminders for signers to sign a document on day 3, 8, 13 and 18 443 * (or less if the document has been digitally signed already) is enabled. 444 * 445 * @return true if reminders are enabled, otherwise false. 446 */ 447 public boolean getAreRemindersEnabled() { 448 return this.areRemindersEnabled; 449 } 450 451 /** 452 * Gets the list of files to create a signing document from. 453 * 454 * @return list of files to create a signing document from. 455 */ 456 public List<BoxFile.Info> getSourceFiles() { 457 return this.sourceFiles; 458 } 459 460 /** 461 * Gets the destination folder to place sign request specific data in (copy of source files, signing log etc.). 462 * 463 * @return destination folder to place sign request specific data in. 464 */ 465 public BoxFolder.Info getParentFolder() { 466 return this.parentFolder; 467 } 468 469 /** 470 * Gets the list of signers for this sign request. 471 * 472 * @return list of signers for this sign request. 473 */ 474 public List<BoxSignRequestSigner> getSigners() { 475 return this.signers; 476 } 477 478 /** 479 * Gets the name of this sign request. 480 * 481 * @return name of this sign request. 482 */ 483 public String getName() { 484 return this.name; 485 } 486 487 /** 488 * Gets the list of prefill tags. 489 * 490 * @return list of prefill tags. 491 */ 492 public List<BoxSignRequestPrefillTag> getPrefillTags() { 493 return this.prefillTags; 494 } 495 496 /** 497 * Gets the number of days after which this request will automatically expire if not completed. 498 * 499 * @return number of days after which this request will automatically expire if not completed. 500 */ 501 public Integer getDaysValid() { 502 return this.daysValid; 503 } 504 505 /** 506 * Gets the reference id in an external system that this sign request is related to. 507 * 508 * @return external id. 509 */ 510 public String getExternalId() { 511 return this.externalId; 512 } 513 514 /** 515 * Gets the URL that can be used by the sign request sender to prepare the document through the UI. 516 * 517 * @return prepare url. 518 */ 519 public String getPrepareUrl() { 520 return this.prepareUrl; 521 } 522 523 /** 524 * Gets the reference to a file that will hold a log of all signer activity for this request. 525 * 526 * @return signing log. 527 */ 528 public BoxFile.Info getSigningLog() { 529 return this.signingLog; 530 } 531 532 /** 533 * Gets the status of the sign request. 534 * 535 * @return sign request's status. 536 */ 537 public BoxSignRequestStatus getStatus() { 538 return this.status; 539 } 540 541 /** 542 * List of files that will be signed, which are copies of the original source files. 543 * A new version of these files are created as signers sign and can be downloaded 544 * at any point in the signing process. 545 * 546 * @return sign files. 547 */ 548 public BoxSignRequestSignFiles getSignFiles() { 549 return this.signFiles; 550 } 551 552 /** 553 * Uses days_valid to calculate the date and time that 554 * the sign request will expire, if unsigned. 555 * 556 * @return auto expires at date. 557 */ 558 public Date getAutoExpireAt() { 559 return this.autoExpireAt; 560 } 561 562 /** 563 * Gets the URL that will be redirected to after the signer completes the sign request. 564 * 565 * @return redirect url. 566 */ 567 public String getRedirectUrl() { 568 return this.redirectUrl; 569 } 570 571 /** 572 * Gets the URL that will be redirected to after the signer declines the sign request. 573 * 574 * @return declined redirect url. 575 */ 576 public String getDeclinedRedirectUrl() { 577 return this.declinedRedirectUrl; 578 } 579 580 /** 581 * {@inheritDoc} 582 */ 583 @Override 584 public BoxSignRequest getResource() { 585 return BoxSignRequest.this; 586 } 587 588 /** 589 * {@inheritDoc} 590 */ 591 @Override 592 void parseJSONMember(JsonObject.Member member) { 593 super.parseJSONMember(member); 594 String memberName = member.getName(); 595 JsonValue value = member.getValue(); 596 try { 597 switch (memberName) { 598 case "is_document_preparation_needed": 599 this.isDocumentPreparationNeeded = value.asBoolean(); 600 break; 601 case "are_text_signatures_enabled": 602 this.areTextSignaturesEnabled = value.asBoolean(); 603 break; 604 case "are_dates_enabled": 605 this.areDatesEnabled = value.asBoolean(); 606 break; 607 case "signature_color": 608 this.signatureColor = BoxSignRequestSignatureColor.fromJSONString(value.asString()); 609 break; 610 case "email_subject": 611 this.emailSubject = value.asString(); 612 break; 613 case "email_message": 614 this.emailMessage = value.asString(); 615 break; 616 case "are_reminders_enabled": 617 this.areRemindersEnabled = value.asBoolean(); 618 break; 619 case "signers": 620 List<BoxSignRequestSigner> signers = new ArrayList<>(); 621 for (JsonValue signerJSON : value.asArray()) { 622 BoxSignRequestSigner signer = new BoxSignRequestSigner(signerJSON.asObject(), getAPI()); 623 signers.add(signer); 624 } 625 this.signers = signers; 626 break; 627 case "source_files": 628 this.sourceFiles = this.getFiles(value.asArray()); 629 break; 630 case "parent_folder": 631 JsonObject folderJSON = value.asObject(); 632 String folderID = folderJSON.get("id").asString(); 633 BoxFolder folder = new BoxFolder(getAPI(), folderID); 634 this.parentFolder = folder.new Info(folderJSON); 635 break; 636 case "name": 637 this.name = value.asString(); 638 break; 639 case "prefill_tags": 640 List<BoxSignRequestPrefillTag> prefillTags = new ArrayList<>(); 641 for (JsonValue prefillTagJSON : value.asArray()) { 642 BoxSignRequestPrefillTag prefillTag = 643 new BoxSignRequestPrefillTag(prefillTagJSON.asObject()); 644 prefillTags.add(prefillTag); 645 } 646 this.prefillTags = prefillTags; 647 break; 648 case "days_valid": 649 this.daysValid = value.asInt(); 650 break; 651 case "external_id": 652 this.externalId = value.asString(); 653 break; 654 case "prepare_url": 655 this.prepareUrl = value.asString(); 656 break; 657 case "signing_log": 658 JsonObject signingLogJSON = value.asObject(); 659 String fileID = signingLogJSON.get("id").asString(); 660 BoxFile file = new BoxFile(getAPI(), fileID); 661 this.signingLog = file.new Info(signingLogJSON); 662 break; 663 case "status": 664 this.status = BoxSignRequestStatus.fromJSONString(value.asString()); 665 break; 666 case "sign_files": 667 JsonObject signFilesJSON = value.asObject(); 668 JsonValue filesArray = signFilesJSON.get("files"); 669 List<BoxFile.Info> signFiles = this.getFiles(filesArray); 670 boolean isReadyForDownload = signFilesJSON.get("is_ready_for_download").asBoolean(); 671 this.signFiles = new BoxSignRequestSignFiles(signFiles, isReadyForDownload); 672 break; 673 case "auto_expire_at": 674 this.autoExpireAt = BoxDateFormat.parse(value.asString()); 675 break; 676 case "redirect_url": 677 this.redirectUrl = value.asString(); 678 break; 679 case "declined_redirect_url": 680 this.declinedRedirectUrl = value.asString(); 681 break; 682 default: 683 return; 684 } 685 } catch (Exception e) { 686 throw new BoxDeserializationException(memberName, value.toString(), e); 687 } 688 } 689 690 private List<BoxFile.Info> getFiles(JsonValue filesArray) { 691 List<BoxFile.Info> files = new ArrayList<>(); 692 for (JsonValue fileJSON : filesArray.asArray()) { 693 String fileID = fileJSON.asObject().get("id").asString(); 694 BoxFile file = new BoxFile(getAPI(), fileID); 695 files.add(file.new Info(fileJSON.asObject())); 696 } 697 return files; 698 } 699 700 /** 701 * List of files that will be signed, which are copies of the original source files. 702 * A new version of these files are created as signers sign and can be downloaded 703 * at any point in the signing process. 704 */ 705 public class BoxSignRequestSignFiles { 706 private final List<BoxFile.Info> files; 707 private final boolean isReadyToDownload; 708 709 /** 710 * Constructs a BoxSignRequestSignFiles. 711 * 712 * @param files list that signing events will occur on. 713 * @param isReadyToDownload indicating whether a change to the document is processing. 714 */ 715 public BoxSignRequestSignFiles(List<BoxFile.Info> files, boolean isReadyToDownload) { 716 this.files = files; 717 this.isReadyToDownload = isReadyToDownload; 718 } 719 720 /** 721 * Gets the list of files that signing events will occur on - these are copies of the original source files. 722 * 723 * @return list of files. 724 */ 725 public List<BoxFile.Info> getFiles() { 726 return this.files; 727 } 728 729 /** 730 * Gets the flag indicating whether a change to the document is processing and the PDF may be out of date. 731 * It is recommended to wait until processing has finished before downloading the PDF. 732 * Webhooks are not sent until processing has been completed. 733 * 734 * @return true if files are ready to download, otherwise false. 735 */ 736 public boolean getIsReadyToDownload() { 737 return this.isReadyToDownload; 738 } 739 } 740 } 741}