Created
June 27, 2019 14:52
-
-
Save danghica/59c09d92c84a5573a464f5002b12a9aa to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@FunctionalInterface | |
public interface XWWWFormUrlencodedHTTPHandler extends HttpHandler { | |
/** | |
* Handles an HTTP request encoded as <code>x-www-form-urlencoded</code>. | |
* This format takes a set of key-value pairs as its input. This method does | |
* not place requirements on what format is used for the output. | |
* | |
* @param params The parameters given in the request, a set of key/value | |
* pairs. | |
* @param method The method used by the request, most commonly | |
* <code>GET</code> or <code>POST</code>. | |
* @return A pair of {output, HTTP status code}. The output is an arbitrary | |
* string of bytes, which is in turn given a format. | |
* @throws Exception | |
*/ | |
public Pair<Pair<byte[], String>, Integer> process( | |
Parameters params, String method) throws Exception; | |
/** | |
* Translates the given HTTP request into a set of key/value pairs, | |
* processes it, and sends the result back to the HTTP client. | |
* <p> | |
* At present, only the <code>POST</code> HTTP method is supported. Other | |
* methods may become supported in future. | |
* | |
* @param exchange The HTTP exchange object that contains information about | |
* the client and its request. | |
* @throws IOException If something goes wrong reading the stream from the | |
* client or writing the stream to the client | |
*/ | |
@Override | |
public default void handle(HttpExchange exchange) throws IOException { | |
try { | |
Parameters params = new Parameters(); | |
Pair<Pair<byte[], String>, Integer> response; | |
try { | |
if (!exchange.getRequestMethod().equals("POST")) { | |
throw new HTTPResponseException(405, "Request method " | |
+ exchange.getRequestMethod() + " is unsupported"); | |
} | |
try (InputStream body = exchange.getRequestBody()) { | |
decodeInputStream(body, params); | |
} | |
response = process(params, exchange.getRequestMethod()); | |
} catch (HTTPResponseException ex) { | |
Throwable cause = ex.getCause(); | |
response = stringResponse(ex.getMessage() + (cause == null ? "" | |
: ": " + cause.getClass().getSimpleName() | |
+ (cause.getMessage() == null ? "" | |
: ": " + cause.getMessage())), | |
ex.getResponseCode()); | |
} catch (Exception ex) { | |
/* rethrow IOExceptions, as the HttpHandler interface expects */ | |
if (ex instanceof IOException) { | |
throw (IOException) ex; | |
} | |
/* something else went wrong = HTTP 500 */ | |
response = stringResponse(ex.getClass().getSimpleName() | |
+ ": " + ex.getMessage(), 500); | |
} | |
if (response.getFirst().getSecond() != null) { | |
/* specify the format of the reply, unless there isn't one */ | |
exchange.getResponseHeaders().set( | |
"Content-Type", response.getFirst().getSecond()); | |
} | |
byte[] responseBody = response.getFirst().getFirst(); | |
exchange.sendResponseHeaders(response.getSecond(), | |
response.getSecond() == 204 || response.getSecond() == 205 | |
? -1 : responseBody.length); | |
exchange.getResponseBody().write(responseBody); | |
} finally { | |
exchange.close(); | |
} | |
} | |
/** | |
* Reads an input stream in <code>x-www-form-urlencoded</code> format, | |
* storing its key/value pairs into a given map. | |
* | |
* @param stream The stream to read. | |
* @param map The map in which to <code>put</code> the results. If the map | |
* is initially empty, this method will thus populate it into a copy of the | |
* key/value pairs in the input stream. | |
* @throws HTTPResponseException If the data is malformed | |
*/ | |
static void decodeInputStream(InputStream stream, Map<String, String> map) | |
throws HTTPResponseException { | |
Scanner s = new Scanner(stream).useDelimiter("\\&"); | |
s.forEachRemaining((pair) -> { | |
Scanner s2 = new Scanner(pair).useDelimiter("="); | |
String key = s2.next(); | |
try { | |
String value = URLDecoder.decode(s2.next().trim(), "UTF-8"); | |
map.put(key, value); | |
} catch (UnsupportedEncodingException | |
| NoSuchElementException ex) { | |
throw new HTTPResponseException(500, | |
"Could not decode x-www-form-urlencoded data", ex); | |
} | |
}); | |
} | |
/** | |
* Produces a return value for <code>process</code> indicating success, but | |
* with no payload data. | |
* | |
* @return A suitable return value for use by <code>process</code>. | |
*/ | |
public static Pair<Pair<byte[], String>, Integer> voidResponse() { | |
/* 204 = "success, no data" */ | |
return new Pair<>(new Pair<>(new byte[0], null), 204); | |
} | |
/** | |
* Formats a set of key/value pairs as an appropriate return value for | |
* <code>process</code>. This will be returned in the | |
* <code>x-www-form-urlencoded</code> format. | |
* | |
* @param response The set of key/value pairs to respond with. | |
* @param code The HTTP response code to use (e.g. 200 for "OK"). | |
* @return A suitable return value for use by <code>process</code>. | |
* @throws java.io.IOException If one of the strings cannot be encoded (e.g. | |
* due to containing mismatched surrogate pairs) | |
*/ | |
public static Pair<Pair<byte[], String>, Integer> keyValuePairsResponse( | |
Map<String, String> response, int code) throws IOException { | |
return new Pair<>(new Pair<>(urlEncodeMap(response).getBytes("UTF-8"), | |
"text/x-www-form-urlencoded; charset=UTF-8"), code); | |
} | |
/** | |
* Formats a string as an appropriate return value for <code>process</code>. | |
* This will be returned as plaintext. | |
* | |
* @param response The string to respond with. A newline will be appended to | |
* it. | |
* @param code The HTTP response code to use (e.g. 404 for "not found"). | |
* @return A suitable return value for use by <code>process</code>. | |
* @throws java.io.IOException If the string cannot be encoded (e.g. due to | |
* containing mismatched surrogate pairs) | |
*/ | |
public static Pair<Pair<byte[], String>, Integer> stringResponse( | |
String response, int code) throws IOException { | |
return new Pair<>(new Pair<>((response + "\r\n").getBytes("UTF-8"), | |
"text/plain; charset=UTF-8"), code); | |
} | |
/** | |
* Encodes the given <code>Map</code> into | |
* <code>x-www-form-urlencoded</code> format. | |
* | |
* @param map The map to encode. | |
* @return A string encoding <code>map</code>. | |
* @throws UnsupportedEncodingException If the content could not be encoded | |
*/ | |
public static String urlEncodeMap(Map<String, String> map) | |
throws UnsupportedEncodingException { | |
StringBuilder encoded = new StringBuilder(); | |
boolean first = true; | |
for (Map.Entry<String, String> e : map.entrySet()) { | |
if (first) { | |
first = false; | |
} else { | |
encoded.append('&'); | |
} | |
encoded.append(URLEncoder.encode(e.getKey(), "UTF-8")); | |
encoded.append('='); | |
encoded.append(URLEncoder.encode(e.getValue(), "UTF-8")); | |
} | |
encoded.append("\r\n"); | |
return encoded.toString(); | |
} | |
/** | |
* An exception that, if thrown, causes a particular HTTP response. This can | |
* be used for exceptions that are the client's fault, not the server's | |
* fault, to override the default 500 response code. | |
*/ | |
public static class HTTPResponseException extends RuntimeException { | |
/** | |
* Serialisation version. This was originally chosen at random, and | |
* should be changed whenever the class changes in an incompatible way. | |
*/ | |
private static final long serialVersionUID = 0xe3a55e5dff7b8f6eL; | |
/** | |
* The HTTP code that will be used. | |
*/ | |
private final int responseCode; | |
/** | |
* Returns the HTTP response code to use. | |
* | |
* @return The response code. | |
*/ | |
public int getResponseCode() { | |
return responseCode; | |
} | |
/** | |
* Creates an HTTP response exception with a given message and response | |
* code. | |
* | |
* @param responseCode The HTTP status code to respond with. | |
* @param message The plaintext message to send. A newline will be | |
* appended to it. | |
*/ | |
public HTTPResponseException(int responseCode, String message) { | |
super(message); | |
this.responseCode = responseCode; | |
} | |
/** | |
* Creates an HTTP response exception wrapping a given exception, with a | |
* given response code. | |
* | |
* @param responseCode The HTTP status code to respond with. | |
* @param message The message to prefix to the exception's message. | |
* @param cause The exception to wrap. | |
*/ | |
public HTTPResponseException(int responseCode, | |
String message, Throwable cause) { | |
super(message, cause); | |
this.responseCode = responseCode; | |
} | |
} | |
/** | |
* A set of key-value pairs. This is just a | |
* <code>Map<String, String></code>, but contains convenience methods | |
* to automatically report errors if something goes wrong. | |
*/ | |
public static class Parameters extends HashMap<String, String> { | |
/** | |
* Serialisation version. This was originally chosen at random, and | |
* should be changed whenever the class changes in an incompatible way. | |
*/ | |
private static final long serialVersionUID = 0x58e9dc63d8669bfcL; | |
/** | |
* Returns the value corresponding to the given key. If the key is | |
* missing, this is considered an error by the user, and an HTTP 400 | |
* response is returned explaining this. | |
* | |
* @param key The key whose value should be returned. | |
* @return The corresponding value, as a string. | |
* @throws HTTPResponseException If the parameter is missing | |
*/ | |
public String parameter(String key) throws HTTPResponseException { | |
if (containsKey(key)) { | |
return get(key); | |
} else { | |
throw new HTTPResponseException( | |
400, "Required parameter '" + key + "' missing"); | |
} | |
} | |
/** | |
* Returns the value corresponding to the given key, using a given | |
* function to decode it from a string. If the key is missing, or the | |
* decoding function fails, this is considered an error by the user, and | |
* an HTTP 400 response is returned explaining this. | |
* | |
* @param <T> The type of value expected. | |
* @param <X> The exception that's thrown if decoding failed. (If | |
* failure does not produce an exception, this method is unnecessary; | |
* just call <code>parameter</code>, then decode the result.) | |
* @param key The key whose value should be returned. | |
* @param decoder The method that decodes the value from a string into a | |
* value of type <code>T</code>. | |
* @param exception The exception thrown when decoding fails. This must | |
* be given explicitly due to Java's type erasure rules. | |
* @return The corresponding value, as a string. | |
* @throws HTTPResponseException If the parameter is missing or could | |
* not be decoded | |
*/ | |
public <T, X extends Exception> T decodeParameter( | |
String key, ExceptionFunction<String, T, X> decoder, | |
Class<X> exception) { | |
String param = parameter(key); | |
try { | |
return decoder.apply(param); | |
} catch (RuntimeException ex) { | |
if (exception.isAssignableFrom(ex.getClass())) { | |
throw new HTTPResponseException( | |
400, "Could not decode parameter '" + key + "'", ex); | |
} | |
throw ex; | |
} catch (Exception ex) { | |
throw new HTTPResponseException( | |
400, "Could not decode parameter '" + key + "'", ex); | |
} | |
} | |
} | |
/** | |
* Generic interface for a function that can throw an exception. | |
* | |
* @param <A> The function's argument. | |
* @param <R> The function's return type. | |
* @param <X> The type of exception the function can throw. This must be a | |
* checked exception (not a <code>RuntimeException</code> or | |
* <code>Error</code>). | |
*/ | |
@FunctionalInterface | |
public interface ExceptionFunction<A, R, X extends Exception> { | |
/** | |
* decoder Applies the function to a given argument. | |
* | |
* @param arg The function's argument. | |
* @return The function's return value, if it succeeds. | |
* @throws X The exception thrown by the function, if it fails. | |
*/ | |
R apply(A arg) throws X; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment