1   package org.daisy.pipeline.tts.rest;
2   
3   import java.io.IOException;
4   import java.io.InputStream;
5   import java.io.OutputStream;
6   import java.net.HttpURLConnection;
7   import java.net.MalformedURLException;
8   import java.net.URL;
9   import java.util.Arrays;
10  import java.util.HashMap;
11  import java.util.List;
12  
13  /**
14   * REST Request to communicate with remote services (mainly tts services like Google and Amazon).
15   *
16   * @author Nicolas Pavie @ braillenet.org
17   */
18  public class Request<ContentType> {
19  
20  	private static List<String> httpMethods =
21  		Arrays.asList(
22  			"GET","POST","PUT","DELETE","HEAD",
23  			"CONNECT","OPTIONS","TRACE","PATCH");
24  
25  	private HashMap<String,String> headers = new HashMap<String,String>();
26  	private ContentType content = null;
27  	private URL requestURL;
28  	private String method = "GET";
29  	private HttpURLConnection connection;
30  
31  	/**
32  	 * Fully initialized request
33  	 *
34  	 * @param httpMethod possible values: "GET", "POST", "PUT", "DELETE", "HEAD", "CONNECT", "OPTIONS", "TRACE", "PATCH"
35  	 * @param url a string of the complete request URL (including URL parameters like "?voice=smtg)"
36  	 * @param headers HTTP headers of the request
37  	 *                <p>Use {@code null} to unset all headers.</p>
38  	 * @param content content to send with the request (often associated with POST requests)
39  	 *                <p>Use {@code null} for requests without content</p>
40  	 * @throws Exception if the http method is not one of the list above
41  	 * @throws MalformedURLException if the url is not valid
42  	 */
43  	public Request(String httpMethod, String url, HashMap<String,String> headers, ContentType content) throws Exception, MalformedURLException {
44  		this.setMethod(httpMethod);
45  		this.setRequestUrl(url);
46  		this.headers = headers;
47  		this.content = content;
48  	}
49  
50  	/**
51  	 * Add a new header field with specified value to the request
52  	 *
53  	 * @param name name of the header field
54  	 * @param value value set for the field
55  	 */
56  	public void addHeader(String name, String value) {
57  		this.headers.put(name, value);
58  	}
59  
60  	/**
61  	 * Set the request content, to be sent through the request connection output stream
62  	 *
63  	 * @param content content of the request
64  	 */
65  	public void setContent(ContentType content) {
66  		this.content = content;
67  	}
68  
69  	/**
70  	 * Set the request URL.
71  	 *
72  	 * <p>If a previous connection was opened for the current request, close and destroy it.</p>
73  	 */
74  	public void setRequestUrl(String url) throws MalformedURLException {
75  		this.requestURL = new URL(url);
76  		if (this.connection != null) {
77  			this.connection.disconnect();
78  			this.connection = null;
79  		}
80  	}
81  
82  	/**
83  	 * Set the HTTP method to use with the request
84  	 *
85  	 * @param httpMethod one of the following values: "GET", "POST", "PUT", "DELETE", "HEAD", "CONNECT", "OPTIONS", "TRACE", "PATCH"
86  	 * @throws Exception if the HTTP method value is not a valid one
87  	 */
88  	public void setMethod(String httpMethod) throws Exception {
89  		if (!httpMethods.contains(httpMethod.toUpperCase())) {
90  			throw new Exception(httpMethod + " is not a valid HTTP method (valid methods : " + httpMethods.toString() + ")");
91  		}
92  		this.method = httpMethod.toUpperCase();
93  	}
94  
95  	/**
96  	 * Send the request to the requested url and retrieve the server answer as an input stream.
97  	 *
98  	 * @return the input stream, through which data are send back by the server
99  	 * @throws IOException if an error occured during the connection or while sending data to the server
100 	 * @throws InterruptedException if the current thead is interrupted while getting the response from the server
101 	 */
102 	public InputStream send() throws IOException, InterruptedException {
103 		connection = (HttpURLConnection) requestURL.openConnection();
104 		connection.setRequestMethod(method);
105 		if (headers != null) headers.forEach((String key, String value) -> {
106 				connection.setRequestProperty(key, value);
107 			});
108 		if (content != null) {
109 			connection.setDoOutput(true);
110 			try (OutputStream os = connection.getOutputStream()) {
111 				byte[] input = content.toString().getBytes("utf-8");
112 				os.write(input, 0, input.length);
113 			}
114 		} else {
115 			connection.setDoOutput(false);
116 		}
117 		// handle thread interrupts (fired by TTSTimeout)
118 		Thread currentThread = Thread.currentThread();
119 		Thread handleInterrupt = new Thread() {
120 			public void run() {
121 				while (true) {
122 					try {
123 						sleep(1000);
124 					} catch (InterruptedException e) {
125 						return;
126 					}
127 					if (currentThread.isInterrupted()) {
128 						connection.disconnect(); // unblocks connection.getInputStream() below
129 						connection = null;
130 						return;
131 					}
132 				}
133 			}
134 		};
135 		handleInterrupt.start();
136 		try {
137 			return connection.getInputStream();
138 		} catch (IOException e) {
139 			handleInterrupt.interrupt();
140 			if (connection == null)
141 				// cancelled by interrupt handler
142 				throw new InterruptedException("request was interrupted");
143 			else
144 				throw e;
145 		} finally {
146 			handleInterrupt.interrupt();
147 		}
148 	}
149 
150 	/**
151 	 * Get the current connection object
152 	 */
153 	public HttpURLConnection getConnection() {
154 		return connection;
155 	}
156 
157 	/**
158 	 * Cancel the request, disconnecting it from the server
159 	 */
160 	public void cancel() {
161 		if (this.connection != null) {
162 			this.connection.disconnect();
163 			this.connection = null;
164 		}
165 	}
166 }