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