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