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}