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<BoxSignRequestFile>(); 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 BoxJSONRequest request = new BoxJSONRequest(getAPI(), url, "POST"); 230 BoxJSONResponse response = (BoxJSONResponse) request.send(); 231 JsonObject responseJSON = Json.parse(response.getJSON()).asObject(); 232 return new BoxSignRequest.Info(responseJSON); 233 } 234 235 /** 236 * Attempts to resend a Sign Request to all signers that have not signed yet. 237 * There is a 10 minute cooling-off period between each resend request. 238 * If you make a resend call during the cooling-off period, a BoxAPIException will be thrown. 239 */ 240 public void resend() { 241 URL url = SIGN_REQUEST_RESEND_URL_TEMPLATE.buildAlphaWithQuery(getAPI().getBaseURL(), "", this.getID()); 242 BoxJSONRequest request = new BoxJSONRequest(getAPI(), url, "POST"); 243 BoxAPIResponse response = request.send(); 244 response.disconnect(); 245 } 246 247 /** 248 * Represents a status of the sign request. 249 */ 250 public enum BoxSignRequestStatus { 251 252 /** 253 * Converting status. 254 */ 255 Converting("converting"), 256 257 /** 258 * Created status. 259 */ 260 Created("created"), 261 262 /** 263 * Sent status. 264 */ 265 Sent("sent"), 266 267 /** 268 * Viewed status. 269 */ 270 Viewed("viewed"), 271 272 /** 273 * Signed status. 274 */ 275 Signed("signed"), 276 277 /** 278 * Cancelled status. 279 */ 280 Cancelled("cancelled"), 281 282 /** 283 * Declined status. 284 */ 285 Declined("declined"), 286 287 /** 288 * Error converting status. 289 */ 290 ErrorConverting("error_converting"), 291 292 /** 293 * Error sending status. 294 */ 295 ErrorSending("error_sending"), 296 297 /** 298 * Expired status. 299 */ 300 Expired("expired"); 301 302 private final String jsonValue; 303 304 BoxSignRequestStatus(String jsonValue) { 305 this.jsonValue = jsonValue; 306 } 307 308 static BoxSignRequestStatus fromJSONString(String jsonValue) { 309 if ("converting".equals(jsonValue)) { 310 return Converting; 311 } else if ("created".equals(jsonValue)) { 312 return Created; 313 } else if ("sent".equals(jsonValue)) { 314 return Sent; 315 } else if ("viewed".equals(jsonValue)) { 316 return Viewed; 317 } else if ("signed".equals(jsonValue)) { 318 return Signed; 319 } else if ("cancelled".equals(jsonValue)) { 320 return Cancelled; 321 } else if ("declined".equals(jsonValue)) { 322 return Declined; 323 } else if ("error_converting".equals(jsonValue)) { 324 return ErrorConverting; 325 } else if ("error_sending".equals(jsonValue)) { 326 return ErrorSending; 327 } else if ("expired".equals(jsonValue)) { 328 return Expired; 329 } 330 throw new IllegalArgumentException("The provided JSON value isn't a valid BoxSignRequestStatus value."); 331 } 332 } 333 334 /** 335 * Contains information about the Sign Request. 336 */ 337 public class Info extends BoxResource.Info { 338 339 private boolean isDocumentPreparationNeeded; 340 private boolean areTextSignaturesEnabled; 341 private boolean areDatesEnabled; 342 private BoxSignRequestSignatureColor signatureColor; 343 private String emailSubject; 344 private String emailMessage; 345 private boolean areRemindersEnabled; 346 private List<BoxFile.Info> sourceFiles; 347 private BoxFolder.Info parentFolder; 348 private List<BoxSignRequestSigner> signers; 349 private String name; 350 private List<BoxSignRequestPrefillTag> prefillTags; 351 private Integer daysValid; 352 private String externalId; 353 private String prepareUrl; 354 private BoxFile.Info signingLog; 355 private BoxSignRequestStatus status; 356 private BoxSignRequestSignFiles signFiles; 357 private Date autoExpireAt; 358 359 /** 360 * Constructs an empty Info object. 361 */ 362 public Info() { 363 super(); 364 } 365 366 /** 367 * Constructs an Info object by parsing information from a JSON string. 368 * 369 * @param json the JSON string to parse. 370 */ 371 public Info(String json) { 372 super(json); 373 } 374 375 /** 376 * Constructs an Info object using an already parsed JSON object. 377 * 378 * @param jsonObject the parsed JSON object. 379 */ 380 Info(JsonObject jsonObject) { 381 super(jsonObject); 382 } 383 384 /** 385 * Indicates if the sender should receive a prepare_url in the response to complete document 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 and 18 440 * (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, signing log etc.). 459 * 460 * @return destination folder to place sign request specific data in. 461 */ 462 public BoxFolder.Info getParentFolder() { 463 return this.parentFolder; 464 } 465 466 /** 467 * Gets the list of signers for this sign request. 468 * 469 * @return list of signers for this sign request. 470 */ 471 public List<BoxSignRequestSigner> getSigners() { 472 return this.signers; 473 } 474 475 /** 476 * Gets the name of this sign request. 477 * 478 * @return name of this sign request. 479 */ 480 public String getName() { 481 return this.name; 482 } 483 484 /** 485 * Gets the list of prefill tags. 486 * 487 * @return list of prefill tags. 488 */ 489 public List<BoxSignRequestPrefillTag> getPrefillTags() { 490 return this.prefillTags; 491 } 492 493 /** 494 * Gets the number of days after which this request will automatically expire if not completed. 495 * 496 * @return number of days after which this request will automatically expire if not completed. 497 */ 498 public Integer getDaysValid() { 499 return this.daysValid; 500 } 501 502 /** 503 * Gets the reference id in an external system that this sign request is related to. 504 * 505 * @return external id. 506 */ 507 public String getExternalId() { 508 return this.externalId; 509 } 510 511 /** 512 * Gets the URL that can be used by the sign request sender to prepare the document through the UI. 513 * 514 * @return prepare url. 515 */ 516 public String getPrepareUrl() { 517 return this.prepareUrl; 518 } 519 520 /** 521 * Gets the reference to a file that will hold a log of all signer activity for this request. 522 * 523 * @return signing log. 524 */ 525 public BoxFile.Info getSigningLog() { 526 return this.signingLog; 527 } 528 529 /** 530 * Gets the status of the sign request. 531 * 532 * @return sign request's status. 533 */ 534 public BoxSignRequestStatus getStatus() { 535 return this.status; 536 } 537 538 /** 539 * List of files that will be signed, which are copies of the original source files. 540 * A new version of these files are created as signers sign and can be downloaded 541 * at any point in the signing process. 542 * 543 * @return sign files. 544 */ 545 public BoxSignRequestSignFiles getSignFiles() { 546 return this.signFiles; 547 } 548 549 /** 550 * Uses days_valid to calculate the date and time that 551 * the sign request will expire, if unsigned. 552 * 553 * @return auto expires at date. 554 */ 555 public Date getAutoExpireAt() { 556 return this.autoExpireAt; 557 } 558 559 /** 560 * {@inheritDoc} 561 */ 562 @Override 563 public BoxSignRequest getResource() { 564 return BoxSignRequest.this; 565 } 566 567 /** 568 * {@inheritDoc} 569 */ 570 @Override 571 void parseJSONMember(JsonObject.Member member) { 572 super.parseJSONMember(member); 573 String memberName = member.getName(); 574 JsonValue value = member.getValue(); 575 try { 576 if ("is_document_preparation_needed".equals(memberName)) { 577 this.isDocumentPreparationNeeded = value.asBoolean(); 578 } else if ("are_text_signatures_enabled".equals(memberName)) { 579 this.areTextSignaturesEnabled = value.asBoolean(); 580 } else if ("are_dates_enabled".equals(memberName)) { 581 this.areDatesEnabled = value.asBoolean(); 582 } else if ("signature_color".equals(memberName)) { 583 this.signatureColor = BoxSignRequestSignatureColor.fromJSONString(value.asString()); 584 } else if ("email_subject".equals(memberName)) { 585 this.emailSubject = value.asString(); 586 } else if ("email_message".equals(memberName)) { 587 this.emailMessage = value.asString(); 588 } else if ("are_reminders_enabled".equals(memberName)) { 589 this.areRemindersEnabled = value.asBoolean(); 590 } else if ("signers".equals(memberName)) { 591 List<BoxSignRequestSigner> signers = new ArrayList<BoxSignRequestSigner>(); 592 for (JsonValue signerJSON : value.asArray()) { 593 BoxSignRequestSigner signer = new BoxSignRequestSigner(signerJSON.asObject(), getAPI()); 594 signers.add(signer); 595 } 596 this.signers = signers; 597 } else if ("source_files".equals(memberName)) { 598 this.sourceFiles = this.getFiles(value.asArray()); 599 } else if ("parent_folder".equals(memberName)) { 600 JsonObject folderJSON = value.asObject(); 601 String folderID = folderJSON.get("id").asString(); 602 BoxFolder folder = new BoxFolder(getAPI(), folderID); 603 this.parentFolder = folder.new Info(folderJSON); 604 } else if ("name".equals(memberName)) { 605 this.name = value.asString(); 606 } else if ("prefill_tags".equals(memberName)) { 607 List<BoxSignRequestPrefillTag> prefillTags = new ArrayList<BoxSignRequestPrefillTag>(); 608 for (JsonValue prefillTagJSON : value.asArray()) { 609 BoxSignRequestPrefillTag prefillTag = 610 new BoxSignRequestPrefillTag(prefillTagJSON.asObject()); 611 prefillTags.add(prefillTag); 612 } 613 this.prefillTags = prefillTags; 614 } else if ("days_valid".equals(memberName)) { 615 this.daysValid = value.asInt(); 616 } else if ("external_id".equals(memberName)) { 617 this.externalId = value.asString(); 618 } else if ("prepare_url".equals(memberName)) { 619 this.prepareUrl = value.asString(); 620 } else if ("signing_log".equals(memberName)) { 621 JsonObject signingLogJSON = value.asObject(); 622 String fileID = signingLogJSON.get("id").asString(); 623 BoxFile file = new BoxFile(getAPI(), fileID); 624 this.signingLog = file.new Info(signingLogJSON); 625 } else if ("status".equals(memberName)) { 626 this.status = BoxSignRequestStatus.fromJSONString(value.asString()); 627 } else if ("sign_files".equals(memberName)) { 628 JsonObject signFilesJSON = value.asObject(); 629 JsonValue filesArray = signFilesJSON.get("files"); 630 List<BoxFile.Info> signFiles = this.getFiles(filesArray); 631 boolean isReadyForDownload = signFilesJSON.get("is_ready_for_download").asBoolean(); 632 this.signFiles = new BoxSignRequestSignFiles(signFiles, isReadyForDownload); 633 } else if ("auto_expire_at".equals(memberName)) { 634 this.autoExpireAt = BoxDateFormat.parse(value.asString()); 635 } 636 } catch (Exception e) { 637 throw new BoxDeserializationException(memberName, value.toString(), e); 638 } 639 } 640 641 private List<BoxFile.Info> getFiles(JsonValue filesArray) { 642 List<BoxFile.Info> files = new ArrayList<BoxFile.Info>(); 643 for (JsonValue fileJSON : filesArray.asArray()) { 644 String fileID = fileJSON.asObject().get("id").asString(); 645 BoxFile file = new BoxFile(getAPI(), fileID); 646 files.add(file.new Info(fileJSON.asObject())); 647 } 648 return files; 649 } 650 651 /** 652 * List of files that will be signed, which are copies of the original source files. 653 * A new version of these files are created as signers sign and can be downloaded 654 * at any point in the signing process. 655 */ 656 public class BoxSignRequestSignFiles { 657 private List<BoxFile.Info> files; 658 private boolean isReadyToDownload; 659 660 /** 661 * Constructs a BoxSignRequestSignFiles. 662 * 663 * @param files list that signing events will occur on. 664 * @param isReadyToDownload indicating whether a change to the document is processing. 665 */ 666 public BoxSignRequestSignFiles(List<BoxFile.Info> files, boolean isReadyToDownload) { 667 this.files = files; 668 this.isReadyToDownload = isReadyToDownload; 669 } 670 671 /** 672 * Gets the list of files that signing events will occur on - these are copies of the original source files. 673 * 674 * @return list of files. 675 */ 676 public List<BoxFile.Info> getFiles() { 677 return this.files; 678 } 679 680 /** 681 * Gets the flag indicating whether a change to the document is processing and the PDF may be out of date. 682 * It is recommended to wait until processing has finished before downloading the PDF. 683 * Webhooks are not sent until processing has been completed. 684 * 685 * @return true if files are ready to download, otherwise false. 686 */ 687 public boolean getIsReadyToDownload() { 688 return this.isReadyToDownload; 689 } 690 } 691 } 692}