diff --git a/src/main/java/com/kttdevelopment/simplehttpserver/FileRecord.java b/src/main/java/com/kttdevelopment/simplehttpserver/FileRecord.java new file mode 100644 index 0000000..a2eeaf9 --- /dev/null +++ b/src/main/java/com/kttdevelopment/simplehttpserver/FileRecord.java @@ -0,0 +1,89 @@ +package com.kttdevelopment.simplehttpserver; + +import java.nio.charset.StandardCharsets; +import java.util.*; + +/** + * This class represents on set of headers and parameters in a multipart/form-data that is expected of a file input. + * + * @see MultipartFormData + * @see Record + * @see com.kttdevelopment.simplehttpserver.Record.Header + * @since 4.0.0 + * @version 4.0.0 + * @author Ktt Development + */ +public class FileRecord extends Record { + + private final String fileName, contentType; + private final byte[] bytes; + + /** + * Creates a record from a map entry. Throws a {@link NullPointerException} if the entry doesn't have all the required fields for a {@link Record#Record(Map.Entry)}; and a filename and Content-Type field. + * + * @param entry map entry + * + * @since 4.0.0 + * @author Ktt Development + */ + @SuppressWarnings("rawtypes") + FileRecord(final Map.Entry entry){ + super(entry); + + Header header = Objects.requireNonNull(getHeader("Content-Disposition")); + fileName = Objects.requireNonNull(header.getParameter("filename")); + header = Objects.requireNonNull(getHeader("Content-Type")); + contentType = Objects.requireNonNull(header.getHeaderValue()); + bytes = getValue().getBytes(StandardCharsets.UTF_8); + } + + /** + * Returns the file name. + * + * @return file name + * + * @since 4.0.0 + * @author Ktt Development + */ + public final String getFileName(){ + return fileName; + } + + /** + * Returns the content type of the file. + * + * @return content type + * + * @since 4.0.0 + * @author Ktt Development + */ + public final String getContentType(){ + return contentType; + } + + /** + * Returns the file as bytes. + * + * @return file in bytes + * + * @see #getValue() + * @since 4.0.0 + * @author Ktt Development + */ + public final byte[] getBytes(){ + return bytes; + } + + @Override + public String toString(){ + return + "FileRecord" + '{' + + "name" + '=' + '\'' + getName() + '\'' + ", " + + "fileName" + '=' + '\'' + fileName + '\'' + ", " + + "contentType" + '=' + '\'' + contentType + '\'' + ", " + + "value" + '=' + '\'' + Arrays.toString(bytes) + '\'' + ", " + + "headers" + '=' + getHeaders() + + '}'; + } + +} diff --git a/src/main/java/com/kttdevelopment/simplehttpserver/MultipartFormData.java b/src/main/java/com/kttdevelopment/simplehttpserver/MultipartFormData.java new file mode 100644 index 0000000..ea9e62b --- /dev/null +++ b/src/main/java/com/kttdevelopment/simplehttpserver/MultipartFormData.java @@ -0,0 +1,71 @@ +package com.kttdevelopment.simplehttpserver; + +import java.util.Collections; +import java.util.Map; + +/** + * This class represents a POST request map as a multipart/form-data. + * + * @see SimpleHttpExchange + * @see Record + * @see FileRecord + * @since 4.0.0 + * @version 4.0.0 + * @author Ktt Development + */ +public class MultipartFormData { + + private final Map records; + + /** + * Creates a multipart/form-data. + * + * @param records map of records and record keys + * + * @see Record + * @see FileRecord + * @since 4.0.0 + * @author Ktt Development + */ + MultipartFormData(final Map records){ + this.records = Collections.unmodifiableMap(records); + } + + /** + * Returns the record for key or null if none is found. If the record is supposed to be a FileRecord then cast it to {@link FileRecord}. + * + * @param key record key + * @return {@link Record} or {@link FileRecord} or null if none is found. + * + * @see Record + * @see FileRecord + * @since 4.0.0 + * @author Ktt Development + */ + public final Record getRecord(final String key){ + return records.get(key); + } + + /** + * Returns all the records in the multipart/form-data; + * + * @return map of all records + * + * @see Record + * @see FileRecord + * @since 4.0.0 + * @author Ktt Development + */ + public final Map getRecords(){ + return records; + } + + @Override + public String toString(){ + return + "MultipartFormData" + '{' + + "record" + '=' + records + + '}'; + } + +} diff --git a/src/main/java/com/kttdevelopment/simplehttpserver/Record.java b/src/main/java/com/kttdevelopment/simplehttpserver/Record.java new file mode 100644 index 0000000..15f27f2 --- /dev/null +++ b/src/main/java/com/kttdevelopment/simplehttpserver/Record.java @@ -0,0 +1,199 @@ +package com.kttdevelopment.simplehttpserver; + +import java.util.*; + +/** + * This class represents one set of headers and parameters in a multipart/form-data. + * + * @see MultipartFormData + * @see FileRecord + * @see Header + * @since 4.0.0 + * @version 4.0.0 + * @author Ktt Development + */ +public class Record { + + private final Map headers; + private final String name, value; + + /** + * Creates a record from a map entry. Throws a {@link NullPointerException} if the entry doesn't have a: header-name, header-value, and parameters field. + * + * @param entry map entry + * + * @since 4.0.0 + * @author Ktt Development + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + Record(final Map.Entry entry){ + name = Objects.requireNonNull(entry.getKey()); + value = Objects.requireNonNull(entry.getValue().get("value").toString()); + + final Map headers = new HashMap<>(); + Objects.requireNonNull((Map) entry.getValue().get("headers")).forEach((k, v) -> headers.put( + k, + new Header( + Objects.requireNonNull(v.get("header-name")).toString(), + Objects.requireNonNull(v.get("header-value")).toString(), + (Map) Objects.requireNonNull(v.get("parameters")) + ) + )); + this.headers = Collections.unmodifiableMap(headers); + } + + /** + * Returns form input name. + * + * @return form input name + * + * @since 4.0.0 + * @author Ktt Development + */ + public final String getName(){ + return name; + } + + /** + * Returns a specified header. + * + * @param key header key + * @return header + * + * @see Header + * @see #getHeaders() + * @since 4.0.0 + * @author Ktt Development + */ + public final Header getHeader(final String key){ + return headers.get(key); + } + + /** + * Returns all the headers. + * + * @return headers + * + * @see Header + * @see #getHeader(String) + * @since 4.0.0 + * @author Ktt Development + */ + public final Map getHeaders(){ + return headers; + } + + /** + * Returns the value as a string. + * + * @return value + * + * @since 4.0.0 + * @author Ktt Development + */ + public final String getValue(){ + return value; + } + + @Override + public String toString(){ + return + "Record" + '{' + + "name" + '=' + '\'' + name + '\'' + ", " + + "value" + '=' + '\'' + value + '\'' + ", " + + "headers" + '=' + headers + + '}'; + } + + /** + * Represents a header in a multipart/form-data. + * + * @since 4.0.0 + * @version 4.0.0 + * @author Ktt Development + */ + public static class Header { + + private final String headerName, headerValue; + private final Map headerParams; + + /** + * Creates header. + * + * @param headerName header name + * @param headerValue header value + * @param headerParams header parameters + * + * @since 4.0.0 + * @author Ktt Development + */ + Header(final String headerName, final String headerValue, final Map headerParams){ + this.headerName = headerName; + this.headerValue = headerValue; + this.headerParams = Collections.unmodifiableMap(headerParams); + } + + /** + * Returns the header name + * + * @return header name + * + * @since 4.0.0 + * @author Ktt Development + */ + public final String getHeaderName(){ + return headerName; + } + + /** + * Returns the header value + * + * @return header value + * + * @since 4.0.0 + * @author Ktt Development + */ + public final String getHeaderValue(){ + return headerValue; + } + + /** + * Returns specific header parameter. + * + * @param key parameter key + * @return parameter value + * + * @see #getParameters() + * @since 4.0.0 + * @author Ktt Development + */ + public final String getParameter(final String key){ + return headerParams.get(key); + } + + /** + * Returns all the header parameters. + * + * @return header parameters + * + * @see #getParameter(String) + * @since 4.0.0 + * @author Ktt Development + */ + public final Map getParameters(){ + return headerParams; + } + + @Override + public String toString(){ + return + "Header" + '{' + + "headerName" + '=' + '\'' + headerName + '\'' + ", " + + "headerValue" + '=' + '\'' + headerValue + '\'' + ", " + + "headerParams" + '=' + headerParams + + '}'; + } + + } + +} diff --git a/src/main/java/com/kttdevelopment/simplehttpserver/SimpleHttpExchange.java b/src/main/java/com/kttdevelopment/simplehttpserver/SimpleHttpExchange.java index f7e9a96..edc3548 100644 --- a/src/main/java/com/kttdevelopment/simplehttpserver/SimpleHttpExchange.java +++ b/src/main/java/com/kttdevelopment/simplehttpserver/SimpleHttpExchange.java @@ -242,7 +242,8 @@ static SimpleHttpExchange create(final HttpExchange exchange){ * @return POST request as a string * * @see HttpExchange#getRequestBody() - * @see #getPostMap() + * @see #getPostMap( + * @see #getMultipartFormData() * @see #hasPost() * @since 02.00.00 * @author Ktt Development @@ -255,6 +256,7 @@ static SimpleHttpExchange create(final HttpExchange exchange){ * @return POST request as a map * * @see #getRawPost() + * @see #getMultipartFormData() * @see #hasPost() * @since 02.00.00 * @author Ktt Development @@ -262,6 +264,20 @@ static SimpleHttpExchange create(final HttpExchange exchange){ @SuppressWarnings("rawtypes") public abstract Map getPostMap(); + /** + * Returns a multipart/form-data as an object or null if there is none. + * + * @return POST request as a multipart/form-data + * + * @see MultipartFormData + * @see #getRawPost() + * @see #getPostMap() + * @see #hasPost() + * @since 4.0.0 + * @author Ktt Development + */ + public abstract MultipartFormData getMultipartFormData(); + /** * Returns if there is a POST request. * @@ -269,6 +285,7 @@ static SimpleHttpExchange create(final HttpExchange exchange){ * * @see #getRawPost() * @see #getPostMap() + * @see #getMultipartFormData() * @since 02.00.00 * @author Ktt Development */ diff --git a/src/main/java/com/kttdevelopment/simplehttpserver/SimpleHttpExchangeImpl.java b/src/main/java/com/kttdevelopment/simplehttpserver/SimpleHttpExchangeImpl.java index 2aac341..0d3b6ed 100644 --- a/src/main/java/com/kttdevelopment/simplehttpserver/SimpleHttpExchangeImpl.java +++ b/src/main/java/com/kttdevelopment/simplehttpserver/SimpleHttpExchangeImpl.java @@ -44,6 +44,7 @@ final class SimpleHttpExchangeImpl extends SimpleHttpExchange { private final String rawPost; @SuppressWarnings("rawtypes") private final Map postMap; + private final MultipartFormData multipartFormData; private final boolean hasPost; private final Map cookies; @@ -119,7 +120,7 @@ static SimpleHttpExchange create(final HttpExchange exchange){ } // hasGet = (rawGet = URI.getQuery()) != null; - getMap = hasGet ? parseWwwFormEnc.apply(rawGet) : new HashMap<>(); + getMap = hasGet ? Collections.unmodifiableMap(parseWwwFormEnc.apply(rawGet)) : new HashMap<>(); // String OUT; @@ -142,19 +143,19 @@ static SimpleHttpExchange create(final HttpExchange exchange){ final String startBoundary = "--" + webkitBoundary + "\r\n"; final String endBoundary = "--" + webkitBoundary + "--\r\n"; // the final boundary in the request - postMap = new HashMap<>(); + final Map postMap_buffer = new HashMap<>(); final String[] pairs = OUT.replace(endBoundary,"").split(Pattern.quote(startBoundary)); for(String pair : pairs){ final Map postHeaders = new HashMap<>(); if(pair.contains("\r\n\r\n")){ final String[] headers = pair.substring(0, pair.indexOf("\r\n\r\n")).split("\r\n"); - for (String header : headers) { + for(String header : headers){ final Map headerMap = new HashMap<>(); final Map val = new HashMap<>(); final Matcher headerMatcher = boundaryHeaderPattern.matcher(header); - if (headerMatcher.find()) { + if(headerMatcher.find()){ final Matcher contentDispositionKVPMatcher = contentDispositionKVPPattern.matcher(headerMatcher.group(3)); while (contentDispositionKVPMatcher.find()) val.put(contentDispositionKVPMatcher.group(1), contentDispositionKVPMatcher.group(2)); @@ -170,29 +171,47 @@ static SimpleHttpExchange create(final HttpExchange exchange){ row.put("headers", postHeaders); row.put("value", pair.substring(pair.indexOf("\r\n\r\n")+4, pair.lastIndexOf("\r\n"))); - postMap.put( - ((HashMap) postHeaders.get("Content-Disposition").get("parameters")).get("name"), + postMap_buffer.put( + ((Map) postHeaders.get("Content-Disposition").get("parameters")).get("name"), row ); } } + Map form_buffer = new HashMap<>(); + for(final Map.Entry e : ((Map) postMap_buffer).entrySet()){ + try{ // try to map as file record first + form_buffer.put(e.getKey(), new FileRecord(e)); + }catch(final NullPointerException ignored){ + try{ // try to map a standard record next + form_buffer.put(e.getKey(), new Record(e)); + }catch(final NullPointerException ignored2){} + }catch(final ClassCastException ignored){ + form_buffer = Collections.emptyMap(); + break; + } + } + + postMap = Collections.unmodifiableMap(postMap_buffer); + multipartFormData = form_buffer.isEmpty() ? null : new MultipartFormData(form_buffer); }else{ - postMap = parseWwwFormEnc.apply(rawPost); + postMap = Collections.unmodifiableMap(parseWwwFormEnc.apply(rawPost)); + multipartFormData = null; } }else{ - postMap = new HashMap(); + postMap = Collections.emptyMap(); + multipartFormData = null; } final String rawCookie = requestHeaders.getFirst("Cookie"); - cookies = new HashMap<>(); + final Map cookie_buffer = new HashMap<>(); if(rawCookie != null && !rawCookie.isEmpty()){ final String[] cookedCookie = rawCookie.split("; "); // pair for(final String pair : cookedCookie){ String[] value = pair.split("="); - cookies.put(value[0], value[1]); + cookie_buffer.put(value[0], value[1]); } } - + cookies = Collections.unmodifiableMap(cookie_buffer); outputStream = exchange.getResponseBody(); } @@ -281,6 +300,11 @@ public final Map getPostMap(){ return postMap; } + @Override + public final MultipartFormData getMultipartFormData(){ + return multipartFormData; + } + @Override public final boolean hasPost(){ return hasPost; @@ -302,7 +326,7 @@ public final int getResponseCode(){ @Override public final Map getCookies(){ - return new HashMap<>(cookies); + return cookies; } @Override @@ -438,26 +462,26 @@ public synchronized final void setAttribute(final String name, final Object valu @Override public String toString(){ return - "SimpleHttpExchange" + '{' + - "httpServer" + '=' + httpServer + ", " + - "httpExchange" + '=' + httpExchange + ", " + - "URI" + '=' + URI + ", " + - "publicAddress" + '=' + publicAddr + ", " + - "localAddress" + '=' + localAddr + ", " + - "httpContext" + '=' + httpContext + ", " + - "httpPrincipal" + '=' + httpPrincipal + ", " + - "protocol" + '=' + protocol + ", " + - "requestHeaders" + '=' + requestHeaders + ", " + - "requestMethod" + '=' + requestMethod + ", " + - "responseHeaders" + '=' + getResponseHeaders() + ", " + - "responseCode" + '=' + getResponseCode() + ", " + - "rawGet" + '=' + rawGet + ", " + - "getMap" + '=' + getMap + ", " + - "hasGet" + '=' + hasGet + ", " + - "rawPost" + '=' + rawPost + ", " + - "postMap" + '=' + postMap + ", " + - "hasPost" + '=' + hasPost + ", " + - "cookies" + '=' + cookies + - '}'; + "SimpleHttpExchange" + '{' + + "httpServer" + '=' + httpServer + ", " + + "httpExchange" + '=' + httpExchange + ", " + + "URI" + '=' + URI + ", " + + "publicAddress" + '=' + publicAddr + ", " + + "localAddress" + '=' + localAddr + ", " + + "httpContext" + '=' + httpContext + ", " + + "httpPrincipal" + '=' + httpPrincipal + ", " + + "protocol" + '=' + protocol + ", " + + "requestHeaders" + '=' + requestHeaders + ", " + + "requestMethod" + '=' + requestMethod + ", " + + "responseHeaders" + '=' + getResponseHeaders() + ", " + + "responseCode" + '=' + getResponseCode() + ", " + + "rawGet" + '=' + rawGet + ", " + + "getMap" + '=' + getMap + ", " + + "hasGet" + '=' + hasGet + ", " + + "rawPost" + '=' + rawPost + ", " + + "postMap" + '=' + postMap + ", " + + "hasPost" + '=' + hasPost + ", " + + "cookies" + '=' + cookies + + '}'; } } diff --git a/src/test/java/com/kttdevelopment/simplehttpserver/handlers/file/FileHandlerAddFilesTest.java b/src/test/java/com/kttdevelopment/simplehttpserver/handlers/file/FileHandlerAddFilesTest.java index 0bd1bc2..d81f490 100644 --- a/src/test/java/com/kttdevelopment/simplehttpserver/handlers/file/FileHandlerAddFilesTest.java +++ b/src/test/java/com/kttdevelopment/simplehttpserver/handlers/file/FileHandlerAddFilesTest.java @@ -38,7 +38,7 @@ public final String getName(final File file){ }; final FileHandler handler = new FileHandler(adapter); - @SuppressWarnings("DuplicateExpressions") // multiple files needed + @SuppressWarnings({"DuplicateExpressions", "RedundantSuppression"}) // multiple unique files needed final File[] files = new File[]{ new File(dir, UUID.randomUUID().toString() + ".txt"), new File(dir, UUID.randomUUID().toString() + ".txt") diff --git a/src/test/java/com/kttdevelopment/simplehttpserver/simplehttpexchange/io/SimpleHttpExchangeMultipartFormTest.java b/src/test/java/com/kttdevelopment/simplehttpserver/simplehttpexchange/io/SimpleHttpExchangeMultipartFormTest.java index e9a8784..f6a11c2 100644 --- a/src/test/java/com/kttdevelopment/simplehttpserver/simplehttpexchange/io/SimpleHttpExchangeMultipartFormTest.java +++ b/src/test/java/com/kttdevelopment/simplehttpserver/simplehttpexchange/io/SimpleHttpExchangeMultipartFormTest.java @@ -37,7 +37,7 @@ public final void postMultipartFormData() throws IOException, ExecutionException final String url = "http://localhost:" + port + context ; final String key = "key", value = "value"; - final String fkey = "fileKey", filename = "fileName.txt", fvalue = "fileValue"; + final String fkey = "fileKey", filename = "fileName.txt", fvalue = "fileValue", contentType = "text/plain"; final StringBuilder OUT = new StringBuilder(); OUT.append("--------------------------").append(boundary).append("\r\n"); @@ -45,8 +45,8 @@ public final void postMultipartFormData() throws IOException, ExecutionException OUT.append(value).append("\r\n"); OUT.append("--------------------------").append(boundary).append("\r\n"); OUT.append("Content-Disposition: ").append("form-data; ").append("name=\"").append(fkey).append("\"; "); - OUT.append("filename=\"").append(filename).append('\"').append('\n'); - OUT.append("Content-Type: ").append("text/plain").append("\r\n\r\n"); + OUT.append("filename=\"").append(filename).append('\"').append("\r\n"); + OUT.append("Content-Type: ").append(contentType).append("\r\n\r\n"); OUT.append(fvalue).append("\r\n"); OUT.append("--------------------------").append(boundary).append("--"); @@ -66,9 +66,19 @@ public final void postMultipartFormData() throws IOException, ExecutionException Assertions.assertTrue(exchange.hasPost(), "Exchange was missing client POST map"); Assertions.assertEquals(value, ((Map) exchange.getPostMap().get(key)).get("value"), "Client form value did not match server value"); + Assertions.assertEquals(filename, ((Map) ((Map) ((Map) ((Map) exchange.getPostMap().get(fkey)).get("headers")).get("Content-Disposition")).get("parameters")).get("filename"), "Client file name did not match server value"); + Assertions.assertEquals(contentType, ((Map) ((Map) ((Map) exchange.getPostMap().get(fkey)).get("headers")).get("Content-Type")).get("header-value"), "Client content-type did not match server value"); Assertions.assertEquals(fvalue, ((Map) exchange.getPostMap().get(fkey)).get("value"), "Client file value did not match server value"); + // multipart/form-data schema + + Assertions.assertEquals(value, exchange.getMultipartFormData().getRecord(key).getValue(), "Client form value did not match server value"); + + Assertions.assertEquals(filename, ((FileRecord) exchange.getMultipartFormData().getRecord(fkey)).getFileName(), "Client file name did not match server value"); + Assertions.assertEquals(contentType, ((FileRecord) exchange.getMultipartFormData().getRecord(fkey)).getContentType(), "Client content-type did not match server value"); + Assertions.assertEquals(fvalue, new String(((FileRecord) exchange.getMultipartFormData().getRecord(fkey)).getBytes()), "Client file value did not match server value"); + server.stop(); } diff --git a/src/test/java/com/kttdevelopment/simplehttpserver/simplehttpexchange/io/SimpleHttpExchangePostTest.java b/src/test/java/com/kttdevelopment/simplehttpserver/simplehttpexchange/io/SimpleHttpExchangePostTest.java index ce34151..cd893aa 100644 --- a/src/test/java/com/kttdevelopment/simplehttpserver/simplehttpexchange/io/SimpleHttpExchangePostTest.java +++ b/src/test/java/com/kttdevelopment/simplehttpserver/simplehttpexchange/io/SimpleHttpExchangePostTest.java @@ -50,6 +50,8 @@ public final void postSimple() throws IOException, ExecutionException, Interrupt Assertions.assertTrue(exchange.hasPost(), "Exchange was missing client POST map"); Assertions.assertEquals(queryValue, exchange.getPostMap().get(queryKey), "Exchange POST did not match client POST"); + Assertions.assertNull(exchange.getMultipartFormData(), "A non-multipart/form-data POST should not return one"); + server.stop(); }