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 try (BoxJSONResponse response = 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 /** 158 * Returns all the sign requests. 159 * 160 * @param api the API connection to be used by the resource. 161 * @param fields the fields to retrieve. 162 * @return an iterable with all the sign requests. 163 */ 164 public static Iterable<BoxSignRequest.Info> getAll(final BoxAPIConnection api, String... fields) { 165 return getAll(api, DEFAULT_LIMIT, fields); 166 } 167 168 /** 169 * Returns all the sign requests. 170 * 171 * @param api the API connection to be used by the resource. 172 * @param limit the limit of items per single response. The default value is 100. 173 * @param fields the fields to retrieve. 174 * @return an iterable with all the sign requests. 175 */ 176 public static Iterable<BoxSignRequest.Info> getAll(final BoxAPIConnection api, int limit, String... fields) { 177 QueryStringBuilder queryString = new QueryStringBuilder(); 178 if (fields.length > 0) { 179 queryString.appendParam("fields", fields); 180 } 181 URL url = SIGN_REQUESTS_URL_TEMPLATE.buildWithQuery(api.getBaseURL(), queryString.toString()); 182 return new BoxResourceIterable<BoxSignRequest.Info>(api, url, limit) { 183 184 @Override 185 protected BoxSignRequest.Info factory(JsonObject jsonObject) { 186 BoxSignRequest signRequest = new BoxSignRequest(api, jsonObject.get("id").asString()); 187 return signRequest.new Info(jsonObject); 188 } 189 190 }; 191 } 192 193 private static List<BoxSignRequestFile> toBoxSignRequestFiles(List<BoxFile.Info> sourceFiles) { 194 List<BoxSignRequestFile> files = new ArrayList<>(); 195 for (BoxFile.Info sourceFile : sourceFiles) { 196 BoxSignRequestFile file = BoxSignRequestFile.fromFile(sourceFile); 197 files.add(file); 198 } 199 200 return files; 201 } 202 203 /** 204 * Returns information about this sign request. 205 * 206 * @param fields the fields to retrieve. 207 * @return information about this sign request. 208 */ 209 public BoxSignRequest.Info getInfo(String... fields) { 210 QueryStringBuilder builder = new QueryStringBuilder(); 211 if (fields.length > 0) { 212 builder.appendParam("fields", fields); 213 } 214 URL url = SIGN_REQUEST_URL_TEMPLATE.buildAlphaWithQuery( 215 this.getAPI().getBaseURL(), builder.toString(), this.getID()); 216 BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "GET"); 217 try (BoxJSONResponse response = request.send()) { 218 JsonObject responseJSON = Json.parse(response.getJSON()).asObject(); 219 return new BoxSignRequest.Info(responseJSON); 220 } 221 } 222 223 /** 224 * Cancels a sign request if it has not yet been signed or declined. 225 * Any outstanding signers will no longer be able to sign the document. 226 * 227 * @return the cancelled sign request's info. 228 */ 229 public BoxSignRequest.Info cancel() { 230 URL url = SIGN_REQUEST_CANCEL_URL_TEMPLATE.buildAlphaWithQuery(getAPI().getBaseURL(), "", this.getID()); 231 BoxJSONRequest request = new BoxJSONRequest(getAPI(), url, "POST"); 232 try (BoxJSONResponse response = request.send()) { 233 JsonObject responseJSON = Json.parse(response.getJSON()).asObject(); 234 return new BoxSignRequest.Info(responseJSON); 235 } 236 } 237 238 /** 239 * Attempts to resend a Sign Request to all signers that have not signed yet. 240 * There is a 10 minute cooling-off period between each resend request. 241 * If you make a resend call during the cooling-off period, a BoxAPIException will be thrown. 242 */ 243 public void resend() { 244 URL url = SIGN_REQUEST_RESEND_URL_TEMPLATE.buildAlphaWithQuery(getAPI().getBaseURL(), "", this.getID()); 245 BoxAPIRequest request = new BoxAPIRequest(getAPI(), url, "POST"); 246 request.send().close(); 247 } 248 249 /** 250 * Represents a status of the sign request. 251 */ 252 public enum BoxSignRequestStatus { 253 254 /** 255 * Converting status. 256 */ 257 Converting("converting"), 258 259 /** 260 * Created status. 261 */ 262 Created("created"), 263 264 /** 265 * Sent status. 266 */ 267 Sent("sent"), 268 269 /** 270 * Viewed status. 271 */ 272 Viewed("viewed"), 273 274 /** 275 * Signed status. 276 */ 277 Signed("signed"), 278 279 /** 280 * Cancelled status. 281 */ 282 Cancelled("cancelled"), 283 284 /** 285 * Declined status. 286 */ 287 Declined("declined"), 288 289 /** 290 * Error converting status. 291 */ 292 ErrorConverting("error_converting"), 293 294 /** 295 * Error sending status. 296 */ 297 ErrorSending("error_sending"), 298 299 /** 300 * Expired status. 301 */ 302 Expired("expired"); 303 304 private final String jsonValue; 305 306 BoxSignRequestStatus(String jsonValue) { 307 this.jsonValue = jsonValue; 308 } 309 310 static BoxSignRequestStatus fromJSONString(String jsonValue) { 311 if ("converting".equals(jsonValue)) { 312 return Converting; 313 } else if ("created".equals(jsonValue)) { 314 return Created; 315 } else if ("sent".equals(jsonValue)) { 316 return Sent; 317 } else if ("viewed".equals(jsonValue)) { 318 return Viewed; 319 } else if ("signed".equals(jsonValue)) { 320 return Signed; 321 } else if ("cancelled".equals(jsonValue)) { 322 return Cancelled; 323 } else if ("declined".equals(jsonValue)) { 324 return Declined; 325 } else if ("error_converting".equals(jsonValue)) { 326 return ErrorConverting; 327 } else if ("error_sending".equals(jsonValue)) { 328 return ErrorSending; 329 } else if ("expired".equals(jsonValue)) { 330 return Expired; 331 } 332 throw new IllegalArgumentException("The provided JSON value isn't a valid BoxSignRequestStatus value."); 333 } 334 } 335 336 /** 337 * Contains information about the Sign Request. 338 */ 339 public class Info extends BoxResource.Info { 340 341 private boolean isDocumentPreparationNeeded; 342 private boolean areTextSignaturesEnabled; 343 private boolean areDatesEnabled; 344 private BoxSignRequestSignatureColor signatureColor; 345 private String emailSubject; 346 private String emailMessage; 347 private boolean areRemindersEnabled; 348 private List<BoxFile.Info> sourceFiles; 349 private BoxFolder.Info parentFolder; 350 private List<BoxSignRequestSigner> signers; 351 private String name; 352 private List<BoxSignRequestPrefillTag> prefillTags; 353 private Integer daysValid; 354 private String externalId; 355 private String prepareUrl; 356 private BoxFile.Info signingLog; 357 private BoxSignRequestStatus status; 358 private BoxSignRequestSignFiles signFiles; 359 private Date autoExpireAt; 360 private String redirectUrl; 361 private String declinedRedirectUrl; 362 363 /** 364 * Constructs an empty Info object. 365 */ 366 public Info() { 367 super(); 368 } 369 370 /** 371 * Constructs an Info object by parsing information from a JSON string. 372 * 373 * @param json the JSON string to parse. 374 */ 375 public Info(String json) { 376 super(json); 377 } 378 379 /** 380 * Constructs an Info object using an already parsed JSON object. 381 * 382 * @param jsonObject the parsed JSON object. 383 */ 384 Info(JsonObject jsonObject) { 385 super(jsonObject); 386 } 387 388 /** 389 * Indicates if the sender should receive a prepare_url in the response to complete document preparation via UI. 390 * 391 * @return true if document preparation is needed, otherwise false. 392 */ 393 public boolean getIsDocumentPreparationNeeded() { 394 return this.isDocumentPreparationNeeded; 395 } 396 397 /** 398 * Gets the flag indicating if usage of signatures generated by typing (text) is enabled. 399 * 400 * @return true if text signatures are enabled, otherwise false. 401 */ 402 public boolean getAreTextSignaturesEnabled() { 403 return this.areTextSignaturesEnabled; 404 } 405 406 /** 407 * Gets the flag indicating if ability for signer to add dates is enabled. 408 * 409 * @return true if ability for signer to add dates is enabled, otherwise false. 410 */ 411 public boolean getAreDatesEnabled() { 412 return this.areDatesEnabled; 413 } 414 415 /** 416 * Gets the forced, specific color for the signature. 417 * 418 * @return signature color (blue, black, red). 419 */ 420 public BoxSignRequestSignatureColor getSignatureColor() { 421 return this.signatureColor; 422 } 423 424 /** 425 * Gets the subject of the sign request email. 426 * 427 * @return subject of the sign request email. 428 */ 429 public String getEmailSubject() { 430 return this.emailSubject; 431 } 432 433 /** 434 * Gets the message to include in the sign request email. 435 * 436 * @return message of sign request email. 437 */ 438 public String getEmailMessage() { 439 return this.emailMessage; 440 } 441 442 /** 443 * Gets the flag indicating if sending reminders for signers to sign a document on day 3, 8, 13 and 18 444 * (or less if the document has been digitally signed already) is enabled. 445 * 446 * @return true if reminders are enabled, otherwise false. 447 */ 448 public boolean getAreRemindersEnabled() { 449 return this.areRemindersEnabled; 450 } 451 452 /** 453 * Gets the list of files to create a signing document from. 454 * 455 * @return list of files to create a signing document from. 456 */ 457 public List<BoxFile.Info> getSourceFiles() { 458 return this.sourceFiles; 459 } 460 461 /** 462 * Gets the destination folder to place sign request specific data in (copy of source files, signing log etc.). 463 * 464 * @return destination folder to place sign request specific data in. 465 */ 466 public BoxFolder.Info getParentFolder() { 467 return this.parentFolder; 468 } 469 470 /** 471 * Gets the list of signers for this sign request. 472 * 473 * @return list of signers for this sign request. 474 */ 475 public List<BoxSignRequestSigner> getSigners() { 476 return this.signers; 477 } 478 479 /** 480 * Gets the name of this sign request. 481 * 482 * @return name of this sign request. 483 */ 484 public String getName() { 485 return this.name; 486 } 487 488 /** 489 * Gets the list of prefill tags. 490 * 491 * @return list of prefill tags. 492 */ 493 public List<BoxSignRequestPrefillTag> getPrefillTags() { 494 return this.prefillTags; 495 } 496 497 /** 498 * Gets the number of days after which this request will automatically expire if not completed. 499 * 500 * @return number of days after which this request will automatically expire if not completed. 501 */ 502 public Integer getDaysValid() { 503 return this.daysValid; 504 } 505 506 /** 507 * Gets the reference id in an external system that this sign request is related to. 508 * 509 * @return external id. 510 */ 511 public String getExternalId() { 512 return this.externalId; 513 } 514 515 /** 516 * Gets the URL that can be used by the sign request sender to prepare the document through the UI. 517 * 518 * @return prepare url. 519 */ 520 public String getPrepareUrl() { 521 return this.prepareUrl; 522 } 523 524 /** 525 * Gets the reference to a file that will hold a log of all signer activity for this request. 526 * 527 * @return signing log. 528 */ 529 public BoxFile.Info getSigningLog() { 530 return this.signingLog; 531 } 532 533 /** 534 * Gets the status of the sign request. 535 * 536 * @return sign request's status. 537 */ 538 public BoxSignRequestStatus getStatus() { 539 return this.status; 540 } 541 542 /** 543 * List of files that will be signed, which are copies of the original source files. 544 * A new version of these files are created as signers sign and can be downloaded 545 * at any point in the signing process. 546 * 547 * @return sign files. 548 */ 549 public BoxSignRequestSignFiles getSignFiles() { 550 return this.signFiles; 551 } 552 553 /** 554 * Uses days_valid to calculate the date and time that 555 * the sign request will expire, if unsigned. 556 * 557 * @return auto expires at date. 558 */ 559 public Date getAutoExpireAt() { 560 return this.autoExpireAt; 561 } 562 563 /** 564 * Gets the URL that will be redirected to after the signer completes the sign request. 565 * 566 * @return redirect url. 567 */ 568 public String getRedirectUrl() { 569 return this.redirectUrl; 570 } 571 572 /** 573 * Gets the URL that will be redirected to after the signer declines the sign request. 574 * 575 * @return declined redirect url. 576 */ 577 public String getDeclinedRedirectUrl() { 578 return this.declinedRedirectUrl; 579 } 580 581 /** 582 * {@inheritDoc} 583 */ 584 @Override 585 public BoxSignRequest getResource() { 586 return BoxSignRequest.this; 587 } 588 589 /** 590 * {@inheritDoc} 591 */ 592 @Override 593 void parseJSONMember(JsonObject.Member member) { 594 super.parseJSONMember(member); 595 String memberName = member.getName(); 596 JsonValue value = member.getValue(); 597 try { 598 switch (memberName) { 599 case "is_document_preparation_needed": 600 this.isDocumentPreparationNeeded = value.asBoolean(); 601 break; 602 case "are_text_signatures_enabled": 603 this.areTextSignaturesEnabled = value.asBoolean(); 604 break; 605 case "are_dates_enabled": 606 this.areDatesEnabled = value.asBoolean(); 607 break; 608 case "signature_color": 609 this.signatureColor = BoxSignRequestSignatureColor.fromJSONString(value.asString()); 610 break; 611 case "email_subject": 612 this.emailSubject = value.asString(); 613 break; 614 case "email_message": 615 this.emailMessage = value.asString(); 616 break; 617 case "are_reminders_enabled": 618 this.areRemindersEnabled = value.asBoolean(); 619 break; 620 case "signers": 621 List<BoxSignRequestSigner> signers = new ArrayList<>(); 622 for (JsonValue signerJSON : value.asArray()) { 623 BoxSignRequestSigner signer = new BoxSignRequestSigner(signerJSON.asObject(), getAPI()); 624 signers.add(signer); 625 } 626 this.signers = signers; 627 break; 628 case "source_files": 629 this.sourceFiles = this.getFiles(value.asArray()); 630 break; 631 case "parent_folder": 632 JsonObject folderJSON = value.asObject(); 633 String folderID = folderJSON.get("id").asString(); 634 BoxFolder folder = new BoxFolder(getAPI(), folderID); 635 this.parentFolder = folder.new Info(folderJSON); 636 break; 637 case "name": 638 this.name = value.asString(); 639 break; 640 case "prefill_tags": 641 List<BoxSignRequestPrefillTag> prefillTags = new ArrayList<>(); 642 for (JsonValue prefillTagJSON : value.asArray()) { 643 BoxSignRequestPrefillTag prefillTag = 644 new BoxSignRequestPrefillTag(prefillTagJSON.asObject()); 645 prefillTags.add(prefillTag); 646 } 647 this.prefillTags = prefillTags; 648 break; 649 case "days_valid": 650 this.daysValid = value.asInt(); 651 break; 652 case "external_id": 653 this.externalId = value.asString(); 654 break; 655 case "prepare_url": 656 this.prepareUrl = value.asString(); 657 break; 658 case "signing_log": 659 JsonObject signingLogJSON = value.asObject(); 660 String fileID = signingLogJSON.get("id").asString(); 661 BoxFile file = new BoxFile(getAPI(), fileID); 662 this.signingLog = file.new Info(signingLogJSON); 663 break; 664 case "status": 665 this.status = BoxSignRequestStatus.fromJSONString(value.asString()); 666 break; 667 case "sign_files": 668 JsonObject signFilesJSON = value.asObject(); 669 JsonValue filesArray = signFilesJSON.get("files"); 670 List<BoxFile.Info> signFiles = this.getFiles(filesArray); 671 boolean isReadyForDownload = signFilesJSON.get("is_ready_for_download").asBoolean(); 672 this.signFiles = new BoxSignRequestSignFiles(signFiles, isReadyForDownload); 673 break; 674 case "auto_expire_at": 675 this.autoExpireAt = BoxDateFormat.parse(value.asString()); 676 break; 677 case "redirect_url": 678 this.redirectUrl = value.asString(); 679 break; 680 case "declined_redirect_url": 681 this.declinedRedirectUrl = value.asString(); 682 break; 683 default: 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}