(testsSent);
121 | testsSent.clear();
122 | for(String testValue : testsSentOrig) {
123 | if(testsReceived.contains(testValue)) {
124 | successes++;
125 | }
126 | else {
127 | testsSent.add(testValue);
128 | }
129 | }
130 |
131 | // Update foundAll
132 | foundAll = testsSent.size() == 0;
133 | }
134 |
135 | /**
136 | * Send 5 messages
137 | * @throws InterruptedException
138 | */
139 | private void send() throws InterruptedException {
140 | testsSent.clear();
141 | for(int i=0 ; i<5 ; i++) {
142 | try {
143 | long now = Instant.now().toEpochMilli();
144 | String testValue = "" + now;
145 | send1(testValue);
146 | testsSent.add(testValue);
147 | }
148 | catch(Exception e) {
149 | enqueueErrors ++;
150 | System.out.println(("send: Enqueue error: " + e));
151 | TimeUnit.SECONDS.sleep(30);
152 | }
153 | }
154 | }
155 |
156 | /**
157 | * Send one enqueue request. Errors will be caught by caller
158 | * @param test the test value
159 | */
160 | private void send1(String test) {
161 |
162 | try {
163 | URL url = new URL(DSConfig.ENQUEUE_URL + "?test=" + test);
164 | HttpsURLConnection options = (HttpsURLConnection) url.openConnection();
165 | options.setReadTimeout(1000 * 20);
166 | options.setRequestMethod("GET");
167 | options.setRequestProperty(DSConfig.AWS_ACCOUNT, DSConfig.AWS_SECRET);
168 | String auth = authObject();
169 | if(!auth.equals(null)) {
170 | String basicAuth = "Basic " + new String(Base64.getEncoder().encode(auth.getBytes()));
171 | options.setRequestProperty("Authorization", basicAuth);
172 | }
173 |
174 | int responseCode = options.getResponseCode();
175 | if(responseCode!=HttpsURLConnection.HTTP_OK) {
176 | Assert.fail("send1: GET not worked");
177 | }
178 | }
179 | catch(SocketTimeoutException e) {
180 | Assert.fail(DatePretty.date() + "send1: SocketTimeoutException - failed to read: " + e);
181 | }
182 | catch(IOException e) {
183 | Assert.fail(DatePretty.date() + "send1: IOException - failed to open url connection: " + e);
184 | }
185 | }
186 |
187 | /**
188 | * Returns a string for the HttpsURLConnection request
189 | * @return Authorization that contains DSConfig.BASIC_AUTH_NAME and DSConfig.BASIC_AUTH_PW if exist
190 | */
191 | private String authObject() {
192 | if (!DSConfig.BASIC_AUTH_NAME.equals(null) && DSConfig.BASIC_AUTH_NAME != "{BASIC_AUTH_NAME}"
193 | && !DSConfig.BASIC_AUTH_PW.equals(null) && DSConfig.BASIC_AUTH_PW != "{BASIC_AUTH_PS}" ) {
194 | return DSConfig.BASIC_AUTH_NAME + ":" + DSConfig.BASIC_AUTH_PW;
195 | }
196 | else {
197 | return null;
198 | }
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/src/test/java/com/docusign/example/test/CreateEnvelope.java:
--------------------------------------------------------------------------------
1 | package com.docusign.example.test;
2 |
3 | import java.io.IOException;
4 | import java.util.ArrayList;
5 | import java.util.Arrays;
6 | import java.util.List;
7 | import com.sun.jersey.core.util.Base64;
8 | import com.docusign.esign.api.EnvelopesApi;
9 | import com.docusign.esign.client.ApiClient;
10 | import com.docusign.esign.client.ApiException;
11 | import com.docusign.esign.model.*;
12 | import com.docusign.example.worker.DSConfig;
13 | import com.docusign.example.worker.JWTAuth;
14 |
15 |
16 | class CreateEnvelope extends JWTAuth {
17 |
18 | private static final String DOC_2_DOCX = "World_Wide_Corp_Battle_Plan_Trafalgar.docx";
19 |
20 | private static final String DOC_3_PDF = "World_Wide_Corp_lorem.pdf";
21 | private static final String ENVELOPE_1_DOCUMENT_1 = "" +
22 | "" +
23 | " " +
24 | " " +
25 | " " +
26 | " " +
27 | " World Wide Corp
" +
29 | " Order Processing Division
" +
32 | " Ordered by " + DSConfig.SIGNER_NAME + "
" +
33 | " Email: " + DSConfig.SIGNER_EMAIL + "
" +
34 | " Copy to: " + DSConfig.CC_NAME + ", " + DSConfig.SIGNER_EMAIL + "
" +
35 | " " +
36 | " Candy bonbon pastry jujubes lollipop wafer biscuit biscuit. Topping brownie sesame snaps" +
37 | " sweet roll pie. Croissant danish biscuit soufflé caramels jujubes jelly. Dragée danish caramels lemon" +
38 | " drops dragée. Gummi bears cupcake biscuit tiramisu sugar plum pastry." +
39 | " Dragée gummies applicake pudding liquorice. Donut jujubes oat cake jelly-o. Dessert bear claw chocolate" +
40 | " cake gummies lollipop sugar plum ice cream gummies cheesecake." +
41 | "
" +
42 | " " +
43 | " Agreed: **signature_1**/
" +
44 | " " +
45 | "";
46 |
47 |
48 | public CreateEnvelope(ApiClient apiClient) throws IOException {
49 | super(apiClient);
50 | }
51 |
52 | /**
53 | * method show the usage of
54 | * @return
55 | * @throws ApiException
56 | * @throws IOException
57 | */
58 | public EnvelopeSummary sendEnvelope() throws ApiException, IOException {
59 |
60 | this.checkToken();
61 |
62 | EnvelopeDefinition envelopeDefinition = new EnvelopeDefinition();
63 | envelopeDefinition.setEmailSubject("Document sent from the Test Mode");
64 |
65 | Document doc1 = new Document();
66 | doc1.setDocumentBase64(new String(Base64.encode(ENVELOPE_1_DOCUMENT_1.getBytes())));
67 | doc1.setName("Order acknowledgement");
68 | doc1.setFileExtension("html");
69 | doc1.setDocumentId("1");
70 |
71 | Document doc2 = new Document();
72 | doc2.setDocumentBase64(new String(Base64.encode(DSHelper.readContent(DOC_2_DOCX))));
73 | doc2.setName("Battle Plan");
74 | doc2.setFileExtension("docx");
75 | doc2.setDocumentId("2");
76 |
77 | Document doc3 = new Document();
78 | doc3.setDocumentBase64(new String(Base64.encode(DSHelper.readContent(DOC_3_PDF))));
79 | doc3.setName("Lorem Ipsum");
80 | doc3.setFileExtension("pdf");
81 | doc3.setDocumentId("3");
82 |
83 | // The order in the docs array determines the order in the envelope
84 | envelopeDefinition.setDocuments(Arrays.asList(doc1, doc2, doc3));
85 | // create a signer recipient to sign the document, identified by name and email
86 | // We're setting the parameters via the object creation
87 | Signer signer1 = new Signer();
88 | signer1.setEmail(DSConfig.SIGNER_EMAIL);
89 | signer1.setName(DSConfig.SIGNER_NAME);
90 | signer1.setRecipientId("1");
91 | signer1.setRoutingOrder("1");
92 | // routingOrder (lower means earlier) determines the order of deliveries
93 | // to the recipients. Parallel routing order is supported by using the
94 | // same integer as the order for two or more recipients.
95 |
96 |
97 | // Create signHere fields (also known as tabs) on the documents,
98 | // We're using anchor (autoPlace) positioning
99 | //
100 | // The DocuSign platform seaches throughout your envelope's
101 | // documents for matching anchor strings. So the
102 | // sign_here_2 tab will be used in both document 2 and 3 since they
103 | // use the same anchor string for their "signer 1" tabs.
104 | SignHere signHere1 = new SignHere();
105 | signHere1.setAnchorString("**signature_1**");
106 | signHere1.setAnchorUnits("pixels");
107 | signHere1.setAnchorXOffset("20");
108 | signHere1.anchorYOffset("10");
109 |
110 | SignHere signHere2 = new SignHere();
111 | signHere2.setAnchorString("/sn1/");
112 | signHere2.setAnchorUnits("pixels");
113 | signHere2.setAnchorXOffset("20");
114 | signHere2.anchorYOffset("10");
115 | // Tabs are set per recipient / signer
116 | Tabs tabs = new Tabs();
117 | tabs.setSignHereTabs(Arrays.asList(signHere1, signHere2));
118 | signer1.setTabs(tabs);
119 | // Add the recipients to the envelope object
120 | Recipients recipients = new Recipients();
121 | recipients.setSigners(Arrays.asList(signer1));
122 | envelopeDefinition.setRecipients(recipients);
123 |
124 | // Request that the envelope be sent by setting |status| to "sent".
125 | // To request that the envelope be created as a draft, set to "created"
126 | envelopeDefinition.setStatus("sent");
127 |
128 | // Add the customFields to the envelope
129 | TextCustomField textCustomField = new TextCustomField();
130 | textCustomField.setValue("Test_Mode");
131 | textCustomField.setShow("true");
132 | textCustomField.setName("Sales order");
133 | CustomFields customFields = new CustomFields();
134 | List textCustomFields = new ArrayList();
135 | textCustomFields.add(textCustomField);
136 | customFields.setTextCustomFields(textCustomFields );
137 | envelopeDefinition.setCustomFields(customFields);
138 |
139 |
140 | EnvelopesApi envelopeApi = new EnvelopesApi(this.apiClient);
141 | EnvelopeSummary results = envelopeApi.createEnvelope(this.getAccountId(), envelopeDefinition);
142 |
143 | return results;
144 | }
145 |
146 | }
147 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Java: Connect Worker for AWS
2 |
3 | Repository: [connect-java-worker-aws](https://github.com/docusign/connect-java-worker-aws).
4 |
5 | ## Introduction
6 |
7 | This is an example worker application for
8 | Connect webhook notification messages sent
9 | via the [AWS SQS (Simple Queueing System)](https://aws.amazon.com/sqs/).
10 |
11 | This application receives DocuSign Connect
12 | messages from the queue and then processes them:
13 |
14 | * If the envelope is complete, the application
15 | uses a DocuSign JWT Grant token to retrieve
16 | the envelope's combined set of documents,
17 | and stores them in the `output` directory.
18 |
19 | For this example, the envelope **must**
20 | include an Envelope Custom Field
21 | named `Sales order.` The Sales order field is used
22 | to name the output file.
23 |
24 | ## Architecture
25 |
26 | 
27 |
28 | AWS has [SQS](https://aws.amazon.com/tools/)
29 | SDK libraries for C#, Java, Node.js, Python, Ruby, C++, and Go.
30 |
31 | ## Installation
32 |
33 | ### Introduction
34 | First, install the **Lambda listener** on AWS and set up the SQS queue.
35 |
36 | Then set up this code example to receive and process the messages
37 | received via the SQS queue.
38 |
39 | ### Eclipse installation
40 |
41 | See the [Eclipse instructions](https://github.com/docusign/connect-java-worker-aws/blob/master/docs/Readme.Eclipse.md)
42 |
43 | ### Installing the Lambda Listener
44 |
45 | Install the example
46 | [Connect listener for AWS](https://github.com/docusign/connect-node-listener-aws)
47 | on AWS.
48 | At the end of this step, you will have the
49 | `Queue URL`, `Queue Region` and `Enqueue url` that you need for the next step.
50 |
51 | ### Installing the worker (this repository)
52 |
53 | #### Requirements
54 | Install the latest Long Term Support version of
55 | Java v1.7, v1.8 or later on your system.
56 |
57 | 1. Download or clone this repository.
58 |
59 | 1. Using AWS IAM, create an IAM `User` with
60 | access to your SQS queue.
61 | Record the IAM user's AWS Access Key and Secret.
62 | Configure environment variables
63 | `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` with the
64 | IAM user credentials.
65 |
66 | 1. Configure a DocuSign Integration Key for the application.
67 | The application uses the OAuth JWT Grant flow.
68 | If consent has not been granted to the application by
69 | the user, then the application provides a url
70 | that can be used to grant individual consent.
71 |
72 | 1. Download this repository to a directory.
73 |
74 | 1. Configure `config.properties` or set the
75 | environment variables as indicated in that file.
76 |
77 |
78 | ### Short installation instructions
79 | * Download or clone this repository.
80 | * The project includes a Maven pom file.
81 | * Configure the project's resource file:
82 |
83 | `connect-java-worker-aws/src/main/resources/config.properties`
84 | See the Configuration section, below,
85 | for more information.
86 | * The project's main class is
87 | `com.docusign.example.jwt.AWSWorker`
88 |
89 |
90 | ## Configure the example
91 |
92 | You can configure the example either via a properties file or via
93 | environment variables:
94 |
95 | * **config.properties:** In the **src/main/resources/**
96 | directory, edit the `config.properties` file to update
97 | it with your settings.
98 | More information for the configuration settings is below.
99 | * Or via **environment variables:** export the needed
100 | environment variables.
101 | The variable names in the `config.properties` file
102 | are the same as the needed environment variables.
103 |
104 | **Note:** do not store your Integration Key, private key, or other
105 | private information in your code repository.
106 |
107 | #### Creating the Integration Key
108 | Your DocuSign Integration Key must be configured for a JWT OAuth authentication flow:
109 | * Using the DocuSign Admin tool,
110 | create a public/private key pair for the integration key.
111 | Store the private key
112 | in a secure location. You can use a file or a key vault.
113 | * The example requires the private key. Store the private key in the
114 | `config.properties` file or in the environment variable
115 | `DS_PRIVATE_KEY`.
116 | * Due to Java handling of multi-line strings, add the
117 | text `\n\` at the end of each line of the private key.
118 | See the example in the `config.properties` file.
119 | * If you will be using individual consent grants, you must create a
120 | `Redirect URI` for the key. Any URL can be used. By default, this
121 | example uses `https://www.docusign.com`
122 |
123 | ````
124 | # private key string
125 | # NOTE: the Java config file parser requires that you
126 | # add \n\ at the ending of every line
127 | # DS_PRIVATE_KEY=\n\
128 | -----BEGIN RSA PRIVATE KEY-----\n\
129 | MIIEpAIBAAKCAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n\
130 | MIIEpAIBAAKCAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n\
131 | MIIEpAIBAAKCAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n\
132 | MIIEpAIBAAKCAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n\
133 | MIIEpAIBAAKCAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n\
134 | MIIEpAIBAAKCAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n\
135 | MIIEpAIBAAKCAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n\
136 | ...
137 | UC1WqwKBgQCY/6aZxlWX9XYgsQsnUjhj2aTwr7pCiQuYceIzLTQzy+nz8M4PfCE1\n\
138 | rjRsm6YTpoxh7nuW2qnFfMA58UPs9tonN/z1pr9mKfwmamtPXeMSJeEZUVmh7mNx\n\
139 | PEHgznlGh/vUboCuA4tQOcKytxFfKG4F+jM/g4GH9z46KZOow3Hb6g==\n\
140 | -----END RSA PRIVATE KEY-----
141 | ````
142 | ## Run the examples
143 |
144 | The project's main class is `com.docusign.example.jwt.AWSWorker`
145 |
146 | ## Testing
147 | Configure a DocuSign Connect subscription to send notifications to
148 | the Cloud Function. Create / complete a DocuSign envelope.
149 | The envelope **must include an Envelope Custom Field named "Sales order".**
150 |
151 | * Check the Connect logs for feedback.
152 | * Check the console output of this app for log output.
153 | * Check the `output` directory to see if the envelope's
154 | combined documents and CoC were downloaded.
155 |
156 | For this code example, the
157 | envelope's documents will only be downloaded if
158 | the envelope is `complete` and includes a
159 | `Sales order` custom field.
160 |
161 | ## Unit Tests
162 | Includes three types of testing:
163 | * [SavingEnvelopeTest.cs](UnitTests/SavingEnvelopeTest.cs) allow you to send an envelope to your amazon sqs from the program. The envelope is saved at `output` directory although its status is `sent`.
164 |
165 | * [RunTest.cs](UnitTests/RunTest.cs) divides into two types of tests, both submits tests for 8 hours and updates every hour about the amount of successes or failures that occurred in that hour, the differences between the two are:
166 | * `few` - Submits 5 tests every hour.
167 | * `many` - Submits many tests every hour.
168 |
169 | In order to run the tests you need to first run the program. then choose the wanted test and run it also.
170 |
171 | ## Support, Contributions, License
172 |
173 | Submit support questions to [StackOverflow](https://stackoverflow.com). Use tag `docusignapi`.
174 |
175 | Contributions via Pull Requests are appreciated.
176 | All contributions must use the MIT License.
177 |
178 | This repository uses the MIT license, see the
179 | [LICENSE](https://github.com/docusign/connect-java-worker-aws/blob/master/LICENSE) file.
180 |
--------------------------------------------------------------------------------
/src/main/java/com/docusign/example/worker/AWSWorker.java:
--------------------------------------------------------------------------------
1 | package com.docusign.example.worker;
2 |
3 | import java.text.SimpleDateFormat;
4 | import java.util.Date;
5 | import java.util.LinkedList;
6 | import java.util.List;
7 | import java.util.Queue;
8 | import java.util.concurrent.TimeUnit;
9 | import org.json.JSONException;
10 | import org.json.JSONObject;
11 | import com.amazonaws.auth.AWSStaticCredentialsProvider;
12 | import com.amazonaws.auth.BasicAWSCredentials;
13 | import com.amazonaws.services.sqs.AmazonSQS;
14 | import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
15 | import com.amazonaws.services.sqs.model.Message;
16 | import com.amazonaws.services.sqs.model.ReceiveMessageRequest;
17 | import com.docusign.esign.client.ApiClient;
18 | import com.docusign.esign.client.ApiException;
19 |
20 | public class AWSWorker {
21 |
22 | private static final ApiClient apiClient = new ApiClient();
23 | private static BasicAWSCredentials creds = new BasicAWSCredentials(DSConfig.AWS_ACCOUNT, DSConfig.AWS_SECRET);
24 | private static AmazonSQS queue = AmazonSQSClientBuilder.standard().withCredentials(new AWSStaticCredentialsProvider(creds)).withRegion(DSConfig.QUEUE_REGION).build();
25 | private static Queue checkLogQ = new LinkedList();
26 | private static String queueUrl = DSConfig.QUEUE_URL;
27 | private static boolean restart = true; //restart the program if any error has occurred
28 |
29 | public static void main(String args[]) throws Exception {
30 |
31 | listenForever();
32 |
33 | }
34 |
35 | /**
36 | * The function will listen forever, dispatching incoming notifications
37 | * to the processNotification library.
38 | * See https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/sqs-examples-send-receive-messages.html#sqs-examples-send-receive-messages-receiving
39 | * @throws Exception while trying to sleep for 5 seconds
40 | */
41 | private static void listenForever() throws Exception {
42 | // Check that we can get a DocuSign token
43 | testToken();
44 |
45 | while(true) {
46 | if(restart) {
47 | System.out.println(DatePretty.date() + "Starting queue worker");
48 | restart = false;
49 | // Start the queue worker
50 | startQueue();
51 | }
52 | TimeUnit.SECONDS.sleep(5);
53 | }
54 | }
55 |
56 | /**
57 | * Check that we can get a DocuSign token and handle common error
58 | * cases: ds_configuration not configured, need consent.
59 | * @throws Exception while trying to get access to token
60 | */
61 | private static void testToken() throws Exception {
62 | try {
63 | if(DSConfig.CLIENT_ID.equals("{CLIENT_ID}")) {
64 | System.err.println("Problem: you need to configure this example, either via environment variables (recommended) \n" +
65 | "or via the ds_configuration.js file. \n" +
66 | "See the README file for more information\n\n");
67 | System.exit(0);
68 | }
69 | JWTAuth dsJWTAuth = new JWTAuth(apiClient);
70 | dsJWTAuth.checkToken();
71 | }
72 |
73 | // An API problem
74 | catch (ApiException e) {
75 | // Special handling for consent_required
76 | String message = e.getMessage();
77 | if(message != null && message.contains("consent_required")){
78 | String consent_url = String.format("%s/oauth/auth?response_type=code&scope=%s" +
79 | "&client_id=%s" +
80 | "&redirect_uri=%s",
81 | DSConfig.DS_AUTH_SERVER,
82 | DSConfig.PERMISSION_SCOPES,
83 | DSConfig.CLIENT_ID,
84 | DSConfig.OAUTH_REDIRECT_URI);
85 | System.err.println("\nC O N S E N T R E Q U I R E D" +
86 | "\nAsk the user who will be impersonated to run the following url: " +
87 | "\n"+ consent_url+
88 | "\n\nIt will ask the user to login and to approve access by your application." +
89 | "\nAlternatively, an Administrator can use Organization Administration to" +
90 | "\npre-approve one or more users.");
91 | System.exit(0);
92 | }
93 | else {
94 | // Some other DocuSign API problem
95 | System.err.println(String.format(" Reason: %d", e.getCode()));
96 | System.err.println(String.format(" Error Reponse: %s", e.getResponseBody()));
97 | System.exit(0);
98 | }
99 | }
100 | // Not an API problem
101 | catch(Exception e) {
102 | System.err.println(DatePretty.date() + e.getMessage());
103 | }
104 | }
105 |
106 | /**
107 | * Receive and wait for messages from queue
108 | * @throws Exception catches all types of errors that may occur during the program
109 | */
110 | private static void startQueue() throws Exception {
111 |
112 | try {
113 | // Receive messages from queue, maximum waits for 20 seconds for message
114 | ReceiveMessageRequest receive_request = new ReceiveMessageRequest().clone()
115 | .withMaxNumberOfMessages(10)
116 | .withQueueUrl(queueUrl)
117 | .withWaitTimeSeconds(20);
118 |
119 | while(true) {
120 | addCheckLogQ(DatePretty.date() + "Awaiting a message...");
121 | // The List will contain all the queue messages
122 | List messages = queue.receiveMessage(receive_request).getMessages();
123 | // The amount of messages received
124 | int msgCount = messages.size();
125 | addCheckLogQ(DatePretty.date() + "found " + msgCount + " message(s)");
126 | // If at least one message has been received
127 | if(msgCount!=0) {
128 | printCheckLogQ();
129 | for(Message m: messages) {
130 | messageHandler(m, queue);
131 | }
132 | }
133 | }
134 | }
135 | // Catches all types of errors that may occur during the program
136 | catch(Exception e) {
137 | printCheckLogQ();
138 | System.err.println("\n"+DatePretty.date() + "Queue receive error:");
139 | System.err.println(e.getMessage());
140 | TimeUnit.SECONDS.sleep(5);
141 | // Restart the program
142 | restart = true;
143 | }
144 | }
145 |
146 |
147 | /**
148 | * Maintain the array checkLogQ as a FIFO buffer with length 4.
149 | * When a new entry is added, remove oldest entry and shuffle.
150 | * @param message the message received from queue
151 | */
152 | private static void addCheckLogQ(String message) {
153 | int length = 4;
154 | // If checkLogQ size is smaller than 4 add the message
155 | if(checkLogQ.size() < length) {
156 | checkLogQ.add(message);
157 | }
158 | // If checkLogQ size is bigger than 4
159 | else {
160 | // Remove the oldest message and add the new one
161 | checkLogQ.remove();
162 | checkLogQ.add(message);
163 | }
164 | }
165 |
166 | /**
167 | * Prints all checkLogQ messages to the console
168 | */
169 | private static void printCheckLogQ() {
170 | // Prints all the elements in the checkLogQ
171 | for(String message : checkLogQ) {
172 | System.out.println(message);
173 | }
174 | checkLogQ.clear(); // Reset
175 | }
176 |
177 | /**
178 | * Process a message
179 | * See https://github.com/Azure/azure-sdk-for-js/tree/master/sdk/servicebus/service-bus#register-message-handler
180 | * @param message the received message from queue
181 | * @param queue contains all the info of the AmazonSQS queue
182 | * @throws Exception while creating JSON object or while trying to convert null object to string
183 | */
184 | private static void messageHandler(Message message, AmazonSQS queue) throws Exception {
185 |
186 | String test = "";
187 | String xml = null;
188 | // If there is an error the program will catch it and the JSONCreated will change to false
189 | boolean JSONCreated = true;
190 |
191 |
192 | if(DSConfig.DEBUG.equals("true")) {
193 | String str = "Processing message id " + message.getMessageId();
194 | System.out.println(DatePretty.date() + str);
195 | }
196 | try {
197 | // Parse the information from message body. the information contains contains fields like test and xml
198 | String body = message.getBody();
199 | JSONObject jsonObject = new JSONObject(body);
200 | xml = (String) jsonObject.get("xml");
201 | // Used in the test mode
202 | test = (String) jsonObject.get("test");
203 |
204 | }
205 | // Catch exceptions while trying to create a JSON object
206 | catch(JSONException e) {
207 | System.err.println(DatePretty.date() + e.getMessage());
208 | JSONCreated = false;
209 | }
210 | // Catch java.lang exceptions (trying to convert null to String) - make sure your message contains both those fields
211 | catch(Exception e) {
212 | System.err.println(DatePretty.date() + e.getMessage());
213 | JSONCreated = false;
214 | }
215 | // If JSON object created successfully - continue
216 | if(JSONCreated) {
217 | ProcessNotification.process(test, xml);
218 | }
219 | // If JSON object wasn't created - ignore message
220 | else {
221 | String errorMessage = "Null or bad body in message id " + message.getMessageId() + ". Ignoring.";
222 | System.out.println(DatePretty.date() + errorMessage);
223 | }
224 | // Delete received message from queue
225 | queue.deleteMessage(queueUrl, message.getReceiptHandle());
226 | }
227 | }
228 |
--------------------------------------------------------------------------------
/src/main/java/com/docusign/example/worker/ProcessNotification.java:
--------------------------------------------------------------------------------
1 | package com.docusign.example.worker;
2 |
3 | import com.docusign.esign.api.EnvelopesApi;
4 | import com.docusign.esign.client.ApiClient;
5 | import com.sun.jersey.api.client.ClientHandlerException;
6 | import org.w3c.dom.Document;
7 | import org.w3c.dom.Node;
8 | import org.w3c.dom.NodeList;
9 | import org.xml.sax.InputSource;
10 |
11 | import javax.xml.parsers.DocumentBuilder;
12 | import javax.xml.parsers.DocumentBuilderFactory;
13 | import javax.xml.xpath.XPath;
14 | import javax.xml.xpath.XPathConstants;
15 | import javax.xml.xpath.XPathExpression;
16 | import javax.xml.xpath.XPathFactory;
17 | import java.io.File;
18 | import java.io.FileWriter;
19 | import java.io.StringReader;
20 | import java.nio.file.Files;
21 | import java.nio.file.Path;
22 | import java.nio.file.Paths;
23 | import java.util.concurrent.TimeUnit;
24 | import java.util.regex.Matcher;
25 | import java.util.regex.Pattern;
26 |
27 | public class ProcessNotification {
28 |
29 | private static String envelopeId;
30 | private static String subject;
31 | private static String senderName;
32 | private static String senderEmail;
33 | private static String status;
34 | private static String created;
35 | private static boolean completed;
36 | private static String completedMsg;
37 | private static String orderNumber;
38 | // Access to the current working directory - in order to save the folders in the right path
39 | public static String mainPath = Paths.get(".").toAbsolutePath().normalize().toString();
40 |
41 | /**
42 | * Process the notification message
43 | * @param test checks whether this is a regular message or test mode message
44 | * @param xml contains all the information of the envelope
45 | * @throws Exception failed to create file or failed to save the document - caught at startQueue method
46 | */
47 | public static void process(String test, String xml) throws Exception{
48 |
49 | // Guarding against injection attacks
50 | String pattern = "[^0-9]";
51 | // Create a Pattern object
52 | Pattern regex = Pattern.compile(pattern);
53 | // Now create matcher object.
54 | Matcher matcher = regex.matcher(test);
55 | // Check the incoming test variable to ensure that it ONLY contains the expected data (empty string "", "/break" or integers string)
56 | // matcher.find() equals true when it finds wrong input
57 | boolean validInput = test.equals("/break") || test.isEmpty() || !matcher.find();
58 | if (validInput) {
59 | if(!test.isEmpty()) {
60 | // Message from test mode
61 | processTest(test);
62 | }
63 | }
64 | else {
65 | System.err.println(DatePretty.date() + "Wrong test value: " + test);
66 | System.err.println("test can only be: /break, empty string or integers string");
67 | }
68 |
69 | // In test mode there is no xml sting, should be checked before trying to parse it
70 | if(!xml.isEmpty()) {
71 | DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
72 | // Guarding against an XML external entity injection attack
73 | dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
74 | dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
75 | dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
76 |
77 | InputSource is = new InputSource();
78 | is.setCharacterStream(new StringReader(xml));
79 | DocumentBuilder db = dbf.newDocumentBuilder();
80 | Document doc = db.parse(is);
81 |
82 | // To get the nodes direct children of use //EnvelopeStatus/node_name
83 | XPath xpath = XPathFactory.newInstance().newXPath();
84 | XPathExpression xpathExpression = xpath.compile("//EnvelopeStatus/EnvelopeID");
85 | NodeList envelopeStatusNodes = (NodeList)xpathExpression.evaluate(doc, XPathConstants.NODESET);
86 | Node file = envelopeStatusNodes.item(0);
87 | envelopeId = file.getTextContent();
88 |
89 | xpathExpression = xpath.compile("//EnvelopeStatus/CustomFields/CustomField/Name");
90 | envelopeStatusNodes = (NodeList)xpathExpression.evaluate(doc, XPathConstants.NODESET);
91 | file = envelopeStatusNodes.item(0);
92 | if(file.getTextContent().equals(DSConfig.ENVELOPE_CUSTOM_FIELD)) {
93 | xpathExpression = xpath.compile("//EnvelopeStatus/CustomFields/CustomField/Value");
94 | envelopeStatusNodes = (NodeList)xpathExpression.evaluate(doc, XPathConstants.NODESET);
95 | file = envelopeStatusNodes.item(0);
96 | orderNumber = file.getTextContent();
97 | }
98 | else {
99 | orderNumber=null;
100 | }
101 |
102 | xpathExpression = xpath.compile("//EnvelopeStatus/Status");
103 | envelopeStatusNodes = (NodeList)xpathExpression.evaluate(doc, XPathConstants.NODESET);
104 | file = envelopeStatusNodes.item(0);
105 | status = file.getTextContent();
106 |
107 | if(status.equals("Completed")){
108 | completed = true;
109 | completedMsg = "Completed " + completed;
110 | }
111 | else {
112 | completed = false;
113 | completedMsg = "";
114 | }
115 |
116 | xpathExpression = xpath.compile("//EnvelopeStatus/Subject");
117 | envelopeStatusNodes = (NodeList)xpathExpression.evaluate(doc, XPathConstants.NODESET);
118 | file = envelopeStatusNodes.item(0);
119 | subject = file.getTextContent();
120 |
121 | xpathExpression = xpath.compile("//EnvelopeStatus/UserName");
122 | envelopeStatusNodes = (NodeList)xpathExpression.evaluate(doc, XPathConstants.NODESET);
123 | file = envelopeStatusNodes.item(0);
124 | senderName = file.getTextContent();
125 |
126 | xpathExpression = xpath.compile("//EnvelopeStatus/Email");
127 | envelopeStatusNodes = (NodeList)xpathExpression.evaluate(doc, XPathConstants.NODESET);
128 | file = envelopeStatusNodes.item(0);
129 | senderEmail = file.getTextContent();
130 |
131 | xpathExpression = xpath.compile("//EnvelopeStatus/Created");
132 | envelopeStatusNodes = (NodeList)xpathExpression.evaluate(doc, XPathConstants.NODESET);
133 | file = envelopeStatusNodes.item(0);
134 | created = file.getTextContent();
135 |
136 |
137 | // For debugging, you can print the entire notification
138 | System.out.println("EnvelopeId: " + envelopeId);
139 | System.out.println("Status: " + status);
140 | System.out.println("Order Number: " + orderNumber);
141 | System.out.println("Subject: " + subject);
142 | System.out.println("Sender: " + senderName + ", " + senderEmail);
143 | System.out.println("Sent: " + created + ", " + completedMsg);
144 |
145 | // Step 2. Filter the notifications
146 | boolean ignore = false;
147 | // Guarding against injection attacks
148 | // Check the incoming orderNumber variable to ensure that it ONLY contains the expected data ("Test_Mode" or integers string)
149 | // Envelope might not have Custom field when orderNumber == null
150 | // matcher.find() equals true when it finds wrong input
151 | matcher = regex.matcher(orderNumber);
152 | validInput = orderNumber.equals("Test_Mode") || !matcher.find() || orderNumber == null;
153 | if(validInput) {
154 | // Check if the envelope was sent from the test mode
155 | // If sent from test mode - ok to continue even if the status != Completed
156 | if(!orderNumber.equals("Test_Mode")) {
157 | if(!status.equals("Completed")) {
158 | ignore = true;
159 | if(DSConfig.DEBUG.equals("true")) {
160 | System.err.println(DatePretty.date() + "IGNORED: envelope status is " + status);
161 | }
162 | }
163 | }
164 |
165 | if(orderNumber == null) {
166 | ignore = true;
167 | if(DSConfig.DEBUG.equals("true")) {
168 | System.err.println(DatePretty.date() + "IGNORED: envelope does not have a " +
169 | DSConfig.ENVELOPE_CUSTOM_FIELD + " envelope custom field.");
170 | }
171 | }
172 | }
173 | else {
174 | ignore = true;
175 | System.err.println(DatePretty.date() + "Wrong orderNumber value: " + orderNumber);
176 | System.err.println("orderNumber can only be: Test_Mode or integers string");
177 | }
178 |
179 | // Step 3. (Future) Check that this is not a duplicate notification
180 | // The queuing system delivers on an "at least once" basis. So there is a
181 | // chance that we have already processes this notification.
182 | //
183 | // For this example, we'll just repeat the document fetch if it is duplicate notification
184 |
185 | // Step 4 save the document - it can raise an exception which will be caught at startQueue
186 | if(!ignore) {
187 | saveDoc(envelopeId, orderNumber);
188 | }
189 | }
190 | }
191 |
192 | /**
193 | * Creates a new file that contains the envelopeId and orderNumber
194 | * @param envelopeId parse from the message
195 | * @param orderNumber parse from the message
196 | * @throws Exception failed to create file or failed to save the document
197 | */
198 | private static void saveDoc(String envelopeId, String orderNumber) throws Exception {
199 |
200 | try {
201 | ApiClient dsApiClient = new ApiClient();
202 | JWTAuth dsJWTAuth = new JWTAuth(dsApiClient);
203 | // Checks for the token before calling the function getToken
204 | dsJWTAuth.checkToken();
205 | dsApiClient.setBasePath(dsJWTAuth.getBasePath());
206 | dsApiClient.setAccessToken(dsJWTAuth.getToken(), (long) 600);
207 | //dsApiClient.addDefaultHeader("Authorization", "Bearer " + dsJWTAuth.getToken());
208 | EnvelopesApi envelopesApi = new EnvelopesApi(dsApiClient);
209 |
210 |
211 | // Create the output directory if needed
212 | File file = new File( Paths.get(mainPath, "output").toString());
213 | if (!file.exists()) {
214 | if (!file.mkdir()) {
215 | throw new Exception(DatePretty.date() + "Failed to create directory");
216 | }
217 | }
218 |
219 | byte[] results = envelopesApi.getDocument(dsJWTAuth.getAccountId(), envelopeId, "combined");
220 | Path path = Paths.get(mainPath,"output", DSConfig.OUTPUT_FILE_PREFIX + orderNumber + ".pdf");
221 |
222 | try {
223 | // Create the output file
224 | Files.write(path, results);
225 | // In order to see the file click F5 or right click on output folder at the Eclipse Project Explorer and refresh.
226 | // If the file won't open inside the Eclipse Try Window > Preferences > General > Editors > File Associations.
227 | // Add *.pdf if it is not there. Highlight it and then add an associated editor.
228 | // Select the External programs radio and then Adobe Acrobat Document or another reader program.
229 | // You can also open it from your computer file explorer.
230 | }
231 | // Catch exception if failed to create file
232 | catch(Exception e) {
233 | throw new Exception(DatePretty.date() + "Failed to create file");
234 | }
235 | // Catch exception if BREAK_TEST equals to true or if orderNumber contains "/break"
236 | if(DSConfig.ENABLE_BREAK_TEST.equals("true") && ("" + orderNumber).contains("/break")) {
237 | throw new Exception (DatePretty.date() + "Break test");
238 | }
239 | }
240 | catch (ClientHandlerException e) {
241 | System.err.println("ClientHandlerException: " + e.getMessage());
242 | }
243 |
244 | // Catch exception while fetching and saving docs for envelope
245 | catch(Exception e) {
246 | System.err.println(DatePretty.date() +
247 | "Error while fetching and saving docs for envelope " + envelopeId + ", order " + orderNumber);
248 | System.err.println(e.getMessage());
249 | throw new Exception (DatePretty.date() + "saveDoc error");
250 | }
251 | }
252 |
253 | /**
254 | * Process test details into files
255 | * @param test contains the test number
256 | * @throws Exception if failed to create directory or failed to rename a file name
257 | */
258 | private static void processTest(String test) throws Exception {
259 |
260 | // Exit the program if BREAK_TEST equals to true or if orderNumber contains "/break"
261 | if(DSConfig.ENABLE_BREAK_TEST.equals("true") && ("" + test).contains("/break")){
262 | System.err.println(DatePretty.date() + "BREAKING worker test!");
263 | System.exit(2);
264 | }
265 |
266 | System.out.println("Processing test value " + test.toString());
267 |
268 | // Create the test_messages directory if needed
269 | String testDirName = DSConfig.TEST_OUTPUT_DIR_NAME;
270 | File testDir = new File(Paths.get(mainPath, testDirName).toString());
271 | if (!testDir.exists()) {
272 | if (!testDir.mkdir()) {
273 | throw new Exception(DatePretty.date() + "Failed to create directory");
274 | }
275 | }
276 |
277 | // First shuffle test1 to test2 (if it exists) and so on
278 | for(int i=9 ; i>0 ; i--) {
279 | File oldFile = new File(testDir, "test" + i + ".txt");
280 | File newFile = new File(testDir, "test" + (i+1) + ".txt");
281 | if(oldFile.exists() && !oldFile.equals(null)) {
282 | // Rename the file name - only works if newFile name does not exist
283 | if(newFile.exists()) {
284 | newFile.delete();
285 | }
286 | // Throw exception if could not rename the file name
287 | // Try to rename 3 times before giving up
288 | boolean renameWorked = false;
289 | for(int j=0; j<3 && !renameWorked ; j++) {
290 | if(!oldFile.renameTo(newFile)) {
291 | TimeUnit.MILLISECONDS.sleep(500);
292 | if(j==2) {
293 | throw new Exception(DatePretty.date() + "Could not rename "+ oldFile.getName() + " to " + newFile.getName());
294 | }
295 | }
296 | else{
297 | renameWorked = true;
298 | }
299 | }
300 | }
301 | }
302 |
303 | // The new test message will be placed in test1 - creating new file
304 | File newFile = new File(testDir, "test1.txt");
305 | FileWriter writer = new FileWriter(newFile);
306 | writer.write(test);
307 | System.out.println(DatePretty.date() + "New file created");
308 | writer.close();
309 | }
310 | }
311 |
--------------------------------------------------------------------------------