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