Skip to content

Commit 08fc77f

Browse files
committed
Implemented multipart support
1 parent 0ce0987 commit 08fc77f

11 files changed

Lines changed: 446 additions & 65 deletions

File tree

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@
5757
<artifactId>reflections</artifactId>
5858
<version>0.10.2</version>
5959
</dependency>
60+
<dependency>
61+
<groupId>commons-fileupload</groupId>
62+
<artifactId>commons-fileupload</artifactId>
63+
<version>1.5</version>
64+
</dependency>
6065
</dependencies>
6166

6267
<build>

src/main/java/org/javawebstack/http/router/Exchange.java

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33
import org.javawebstack.abstractdata.*;
44
import org.javawebstack.abstractdata.mapper.Mapper;
55
import org.javawebstack.http.router.adapter.IHTTPSocket;
6+
import org.javawebstack.http.router.multipart.Part;
7+
import org.javawebstack.http.router.multipart.content.InMemoryCache;
8+
import org.javawebstack.http.router.multipart.content.PartContentCache;
9+
import org.javawebstack.http.router.multipart.content.TmpFolderCache;
10+
import org.javawebstack.http.router.util.HeaderValue;
611
import org.javawebstack.http.router.util.MimeType;
712
import org.javawebstack.validator.ValidationContext;
813
import org.javawebstack.validator.ValidationException;
914
import org.javawebstack.validator.ValidationResult;
1015
import org.javawebstack.validator.Validator;
1116

12-
import java.io.ByteArrayOutputStream;
13-
import java.io.IOException;
14-
import java.io.InputStream;
17+
import java.io.*;
1518
import java.nio.charset.StandardCharsets;
1619
import java.util.*;
1720
import java.util.stream.Collectors;
@@ -32,6 +35,7 @@ public static Exchange current() {
3235
private final AbstractObject queryParameters;
3336
private final IHTTPSocket socket;
3437
private final Map<String, Object> attributes = new HashMap<>();
38+
private List<Part> parts = null;
3539

3640
public Exchange(HTTPRouter router, IHTTPSocket socket) {
3741
this.router = router;
@@ -301,12 +305,64 @@ private HTTPMethod getRequestMethodFromSocket(IHTTPSocket socket) {
301305
return socket.getRequestMethod();
302306
}
303307

304-
public MimeType getMimeType() {
305-
String contentType = getContentType().toLowerCase();
306-
if (contentType.contains(";")) {
307-
contentType = contentType.split(";")[0].trim();
308+
/**
309+
* Use with CAUTION! This will load all parts into memory
310+
* @return
311+
*/
312+
public Exchange enableMultipart() {
313+
return enableMultipart(new InMemoryCache());
314+
}
315+
316+
/**
317+
* Use with CAUTION! This will run an expensive cleanup routine on each call and store files on disk
318+
* @param tmpFolder
319+
* @return
320+
*/
321+
public Exchange enableMultipart(File tmpFolder) {
322+
PartContentCache cache = new TmpFolderCache(tmpFolder);
323+
cache.cleanup();
324+
return enableMultipart(cache);
325+
}
326+
327+
public Exchange enableMultipart(PartContentCache cache) {
328+
if(parts != null)
329+
return this;
330+
HeaderValue contentType = new HeaderValue(getContentType());
331+
if(!contentType.getValue().toLowerCase(Locale.ROOT).equals("multipart/form-data"))
332+
return this;
333+
body = new byte[0];
334+
byte[] boundary = contentType.getDirectives().get("boundary").getBytes();
335+
try {
336+
InputStream stream;
337+
if(body != null) {
338+
stream = new ByteArrayInputStream(body);
339+
} else {
340+
stream = socket.getInputStream();
341+
}
342+
parts = Part.parse(stream, boundary, cache);
343+
} catch (Exception ex) {
344+
parts = new ArrayList<>();
308345
}
346+
return this;
347+
}
348+
349+
public boolean isMultipart() {
350+
return parts != null;
351+
}
352+
353+
public List<Part> getParts() {
354+
if(!isMultipart())
355+
throw new IllegalStateException("This is not a multipart request or multipart parsing is not enabled. Use enableMultipart to enable it or check with isMultipart");
356+
return parts;
357+
}
309358

310-
return MimeType.byMimeType(contentType);
359+
public Part getPart(String name) {
360+
return getParts().stream().filter(p -> name.equals(p.getName())).findFirst().orElse(null);
311361
}
362+
363+
public MimeType getMimeType() {
364+
HeaderValue contentType = new HeaderValue(getContentType());
365+
return MimeType.byMimeType(contentType.getValue().toLowerCase(Locale.ROOT));
366+
}
367+
312368
}

src/main/java/org/javawebstack/http/router/HTTPRouter.java

Lines changed: 72 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.javawebstack.abstractdata.mapper.naming.NamingPolicy;
55
import org.javawebstack.http.router.adapter.IHTTPSocketServer;
66
import org.javawebstack.http.router.handler.*;
7+
import org.javawebstack.http.router.multipart.content.PartContentCache;
78
import org.javawebstack.http.router.router.DefaultRouteAutoInjector;
89
import org.javawebstack.http.router.router.Route;
910
import org.javawebstack.http.router.router.RouteAutoInjector;
@@ -43,6 +44,7 @@ public class HTTPRouter implements RouteParamTransformerProvider {
4344
private final Map<String, AfterRequestHandler> afterMiddleware = new HashMap<>();
4445
private Function<Class<?>, Object> controllerInitiator = this::defaultControllerInitiator;
4546
private boolean formMethods = true;
47+
private PartContentCache multipartContentCache;
4648

4749
public HTTPRouter(IHTTPSocketServer server) {
4850
this.server = server;
@@ -300,78 +302,86 @@ public void stop() {
300302
public void execute(Exchange exchange) {
301303
Exchange.exchanges.set(exchange);
302304
try {
303-
Object response = null;
304305
try {
305-
for (RequestInterceptor ic : beforeInterceptors) {
306-
if (ic.intercept(exchange)) {
307-
exchange.close();
308-
Exchange.exchanges.remove();
309-
return;
310-
}
311-
}
312-
middlewares:
313-
for (Route route : beforeRoutes) {
314-
Map<String, Object> pathVariables = route.match(exchange);
315-
if (pathVariables == null)
316-
continue;
317-
exchange.getPathVariables().putAll(pathVariables);
318-
for (RequestHandler handler : route.getHandlers()) {
319-
try {
320-
response = handler.handle(exchange);
321-
} catch (Throwable ex) {
322-
response = exceptionHandler.handle(exchange, ex);
306+
if(multipartContentCache != null)
307+
exchange.enableMultipart(multipartContentCache);
308+
Object response = null;
309+
try {
310+
for (RequestInterceptor ic : beforeInterceptors) {
311+
if (ic.intercept(exchange)) {
312+
exchange.close();
313+
Exchange.exchanges.remove();
314+
return;
323315
}
324-
if (response != null)
325-
break middlewares;
326316
}
327-
}
328-
exchange.getPathVariables().clear();
329-
if (response == null) {
330-
routes:
331-
for (Route route : routes) {
317+
middlewares:
318+
for (Route route : beforeRoutes) {
332319
Map<String, Object> pathVariables = route.match(exchange);
333320
if (pathVariables == null)
334321
continue;
335322
exchange.getPathVariables().putAll(pathVariables);
336323
for (RequestHandler handler : route.getHandlers()) {
337-
response = handler.handle(exchange);
338-
if (exchange.getMethod() == HTTPMethod.WEBSOCKET) {
339-
Exchange.exchanges.remove();
340-
return;
324+
try {
325+
response = handler.handle(exchange);
326+
} catch (Throwable ex) {
327+
response = exceptionHandler.handle(exchange, ex);
341328
}
342329
if (response != null)
343-
break routes;
330+
break middlewares;
331+
}
332+
}
333+
exchange.getPathVariables().clear();
334+
if (response == null) {
335+
routes:
336+
for (Route route : routes) {
337+
Map<String, Object> pathVariables = route.match(exchange);
338+
if (pathVariables == null)
339+
continue;
340+
exchange.getPathVariables().putAll(pathVariables);
341+
for (RequestHandler handler : route.getHandlers()) {
342+
response = handler.handle(exchange);
343+
if (exchange.getMethod() == HTTPMethod.WEBSOCKET) {
344+
Exchange.exchanges.remove();
345+
return;
346+
}
347+
if (response != null)
348+
break routes;
349+
}
350+
exchange.getPathVariables().clear();
344351
}
345-
exchange.getPathVariables().clear();
346352
}
353+
} catch (Throwable ex) {
354+
response = exceptionHandler.handle(exchange, ex);
347355
}
348-
} catch (Throwable ex) {
349-
response = exceptionHandler.handle(exchange, ex);
350-
}
351-
if (response == null)
352-
response = notFoundHandler.handle(exchange);
353-
exchange.getPathVariables().clear();
354-
for (Route route : afterRoutes) {
355-
Map<String, Object> pathVariables = route.match(exchange);
356-
if (pathVariables == null)
357-
continue;
358-
exchange.getPathVariables().putAll(pathVariables);
359-
for (AfterRequestHandler handler : route.getAfterHandlers())
360-
response = handler.handleAfter(exchange, response);
356+
if (response == null)
357+
response = notFoundHandler.handle(exchange);
361358
exchange.getPathVariables().clear();
359+
for (Route route : afterRoutes) {
360+
Map<String, Object> pathVariables = route.match(exchange);
361+
if (pathVariables == null)
362+
continue;
363+
exchange.getPathVariables().putAll(pathVariables);
364+
for (AfterRequestHandler handler : route.getAfterHandlers())
365+
response = handler.handleAfter(exchange, response);
366+
exchange.getPathVariables().clear();
367+
}
368+
if (response != null)
369+
exchange.write(transformResponse(exchange, response));
370+
if (exchange.getMethod() != HTTPMethod.WEBSOCKET)
371+
exchange.close();
372+
Exchange.exchanges.remove();
373+
return;
374+
} catch (Throwable ex) {
375+
try {
376+
exchange.write(transformResponse(exchange, exceptionHandler.handle(exchange, ex)));
377+
} catch (Throwable ex2) {
378+
exchange.status(500);
379+
logger.log(Level.SEVERE, ex2, () -> "An error occured in the exception handler!");
380+
}
362381
}
363-
if (response != null)
364-
exchange.write(transformResponse(exchange, response));
365-
if (exchange.getMethod() != HTTPMethod.WEBSOCKET)
366-
exchange.close();
367-
Exchange.exchanges.remove();
368-
return;
369-
} catch (Throwable ex) {
370-
try {
371-
exchange.write(transformResponse(exchange, exceptionHandler.handle(exchange, ex)));
372-
} catch (Throwable ex2) {
373-
logger.log(Level.SEVERE, ex2, () -> "An error occured in the exception handler!");
374-
}
382+
} catch (Exception ex) {
383+
// This should never be reached, just added this as a precaution
384+
logger.log(Level.SEVERE, ex, () -> "An unexpected error occured in the exception handling of the exception handler (probably while setting the status)");
375385
}
376386
Exchange.exchanges.remove();
377387
exchange.close();
@@ -411,6 +421,11 @@ public ExceptionHandler getExceptionHandler() {
411421
return exceptionHandler;
412422
}
413423

424+
public HTTPRouter enableMultipart(PartContentCache cache) {
425+
this.multipartContentCache = cache;
426+
return this;
427+
}
428+
414429
public boolean isFormMethods() {
415430
return formMethods;
416431
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package org.javawebstack.http.router.multipart;
2+
3+
import org.apache.commons.fileupload.MultipartStream;
4+
import org.javawebstack.http.router.Exchange;
5+
import org.javawebstack.http.router.multipart.content.PartContent;
6+
import org.javawebstack.http.router.multipart.content.PartContentCache;
7+
import org.javawebstack.http.router.util.HeaderValue;
8+
9+
import java.io.ByteArrayOutputStream;
10+
import java.io.IOException;
11+
import java.io.InputStream;
12+
import java.util.*;
13+
import java.util.stream.Collectors;
14+
import java.util.stream.Stream;
15+
16+
public class Part {
17+
18+
final Map<String, String> headers;
19+
final PartContent content;
20+
String name;
21+
String contentType;
22+
23+
public Part(Map<String, String> headers, PartContent content) {
24+
this.headers = headers;
25+
this.content = content;
26+
if(headers.containsKey("content-disposition")) {
27+
HeaderValue contentDisposition = new HeaderValue(headers.get("content-disposition"));
28+
if(contentDisposition.getDirectives().containsKey("name"))
29+
name = contentDisposition.getDirectives().get("name");
30+
}
31+
if(headers.containsKey("content-type")) {
32+
contentType = new HeaderValue(headers.get("content-type")).getValue();
33+
}
34+
}
35+
36+
public static List<Part> parse(InputStream inputStream, byte[] boundary, PartContentCache cache) throws IOException {
37+
MultipartStream stream = new MultipartStream(inputStream, boundary, 1024, null);
38+
List<Part> parts = new ArrayList<>();
39+
if(stream.skipPreamble()) {
40+
do {
41+
Map<String, String> headers = Stream.of(
42+
stream.readHeaders().split("\r?\n")
43+
)
44+
.filter(l -> !l.trim().isEmpty())
45+
.map(l -> l.split(": ", 2))
46+
.collect(Collectors.toMap(
47+
a -> a[0].toLowerCase(Locale.ROOT),
48+
a -> a.length == 2 ? a[1] : ""
49+
));
50+
PartContent c = cache.store(os -> {
51+
try {
52+
stream.readBodyData(os);
53+
} catch (IOException e) {
54+
throw new RuntimeException(e);
55+
}
56+
});
57+
parts.add(new Part(headers, c));
58+
} while (stream.readBoundary());
59+
}
60+
return parts;
61+
}
62+
63+
public Map<String, String> getHeaders() {
64+
return headers;
65+
}
66+
67+
public InputStream getContentStream() {
68+
return content.read();
69+
}
70+
71+
public String getName() {
72+
return name;
73+
}
74+
75+
public String getContentType() {
76+
return contentType;
77+
}
78+
79+
public byte[] getContentBytes() {
80+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
81+
InputStream stream = getContentStream();
82+
int b;
83+
try {
84+
while ((b = stream.read()) != -1)
85+
baos.write(b);
86+
stream.close();
87+
} catch (IOException ex) {
88+
throw new RuntimeException(ex);
89+
}
90+
return baos.toByteArray();
91+
}
92+
93+
public void discard() {
94+
content.discard();
95+
}
96+
97+
}

0 commit comments

Comments
 (0)