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}