Skip to content

Commit

Permalink
Implemented multipart support
Browse files Browse the repository at this point in the history
  • Loading branch information
JanHolger committed Sep 3, 2023
1 parent 0ce0987 commit 08fc77f
Show file tree
Hide file tree
Showing 11 changed files with 446 additions and 65 deletions.
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@
<artifactId>reflections</artifactId>
<version>0.10.2</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.5</version>
</dependency>
</dependencies>

<build>
Expand Down
72 changes: 64 additions & 8 deletions src/main/java/org/javawebstack/http/router/Exchange.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
import org.javawebstack.abstractdata.*;
import org.javawebstack.abstractdata.mapper.Mapper;
import org.javawebstack.http.router.adapter.IHTTPSocket;
import org.javawebstack.http.router.multipart.Part;
import org.javawebstack.http.router.multipart.content.InMemoryCache;
import org.javawebstack.http.router.multipart.content.PartContentCache;
import org.javawebstack.http.router.multipart.content.TmpFolderCache;
import org.javawebstack.http.router.util.HeaderValue;
import org.javawebstack.http.router.util.MimeType;
import org.javawebstack.validator.ValidationContext;
import org.javawebstack.validator.ValidationException;
import org.javawebstack.validator.ValidationResult;
import org.javawebstack.validator.Validator;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
Expand All @@ -32,6 +35,7 @@ public static Exchange current() {
private final AbstractObject queryParameters;
private final IHTTPSocket socket;
private final Map<String, Object> attributes = new HashMap<>();
private List<Part> parts = null;

public Exchange(HTTPRouter router, IHTTPSocket socket) {
this.router = router;
Expand Down Expand Up @@ -301,12 +305,64 @@ private HTTPMethod getRequestMethodFromSocket(IHTTPSocket socket) {
return socket.getRequestMethod();
}

public MimeType getMimeType() {
String contentType = getContentType().toLowerCase();
if (contentType.contains(";")) {
contentType = contentType.split(";")[0].trim();
/**
* Use with CAUTION! This will load all parts into memory
* @return
*/
public Exchange enableMultipart() {
return enableMultipart(new InMemoryCache());
}

/**
* Use with CAUTION! This will run an expensive cleanup routine on each call and store files on disk
* @param tmpFolder
* @return
*/
public Exchange enableMultipart(File tmpFolder) {
PartContentCache cache = new TmpFolderCache(tmpFolder);
cache.cleanup();
return enableMultipart(cache);
}

public Exchange enableMultipart(PartContentCache cache) {
if(parts != null)
return this;
HeaderValue contentType = new HeaderValue(getContentType());
if(!contentType.getValue().toLowerCase(Locale.ROOT).equals("multipart/form-data"))
return this;
body = new byte[0];
byte[] boundary = contentType.getDirectives().get("boundary").getBytes();
try {
InputStream stream;
if(body != null) {
stream = new ByteArrayInputStream(body);
} else {
stream = socket.getInputStream();
}
parts = Part.parse(stream, boundary, cache);
} catch (Exception ex) {
parts = new ArrayList<>();
}
return this;
}

public boolean isMultipart() {
return parts != null;
}

public List<Part> getParts() {
if(!isMultipart())
throw new IllegalStateException("This is not a multipart request or multipart parsing is not enabled. Use enableMultipart to enable it or check with isMultipart");
return parts;
}

return MimeType.byMimeType(contentType);
public Part getPart(String name) {
return getParts().stream().filter(p -> name.equals(p.getName())).findFirst().orElse(null);
}

public MimeType getMimeType() {
HeaderValue contentType = new HeaderValue(getContentType());
return MimeType.byMimeType(contentType.getValue().toLowerCase(Locale.ROOT));
}

}
129 changes: 72 additions & 57 deletions src/main/java/org/javawebstack/http/router/HTTPRouter.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.javawebstack.abstractdata.mapper.naming.NamingPolicy;
import org.javawebstack.http.router.adapter.IHTTPSocketServer;
import org.javawebstack.http.router.handler.*;
import org.javawebstack.http.router.multipart.content.PartContentCache;
import org.javawebstack.http.router.router.DefaultRouteAutoInjector;
import org.javawebstack.http.router.router.Route;
import org.javawebstack.http.router.router.RouteAutoInjector;
Expand Down Expand Up @@ -43,6 +44,7 @@ public class HTTPRouter implements RouteParamTransformerProvider {
private final Map<String, AfterRequestHandler> afterMiddleware = new HashMap<>();
private Function<Class<?>, Object> controllerInitiator = this::defaultControllerInitiator;
private boolean formMethods = true;
private PartContentCache multipartContentCache;

public HTTPRouter(IHTTPSocketServer server) {
this.server = server;
Expand Down Expand Up @@ -300,78 +302,86 @@ public void stop() {
public void execute(Exchange exchange) {
Exchange.exchanges.set(exchange);
try {
Object response = null;
try {
for (RequestInterceptor ic : beforeInterceptors) {
if (ic.intercept(exchange)) {
exchange.close();
Exchange.exchanges.remove();
return;
}
}
middlewares:
for (Route route : beforeRoutes) {
Map<String, Object> pathVariables = route.match(exchange);
if (pathVariables == null)
continue;
exchange.getPathVariables().putAll(pathVariables);
for (RequestHandler handler : route.getHandlers()) {
try {
response = handler.handle(exchange);
} catch (Throwable ex) {
response = exceptionHandler.handle(exchange, ex);
if(multipartContentCache != null)
exchange.enableMultipart(multipartContentCache);
Object response = null;
try {
for (RequestInterceptor ic : beforeInterceptors) {
if (ic.intercept(exchange)) {
exchange.close();
Exchange.exchanges.remove();
return;
}
if (response != null)
break middlewares;
}
}
exchange.getPathVariables().clear();
if (response == null) {
routes:
for (Route route : routes) {
middlewares:
for (Route route : beforeRoutes) {
Map<String, Object> pathVariables = route.match(exchange);
if (pathVariables == null)
continue;
exchange.getPathVariables().putAll(pathVariables);
for (RequestHandler handler : route.getHandlers()) {
response = handler.handle(exchange);
if (exchange.getMethod() == HTTPMethod.WEBSOCKET) {
Exchange.exchanges.remove();
return;
try {
response = handler.handle(exchange);
} catch (Throwable ex) {
response = exceptionHandler.handle(exchange, ex);
}
if (response != null)
break routes;
break middlewares;
}
}
exchange.getPathVariables().clear();
if (response == null) {
routes:
for (Route route : routes) {
Map<String, Object> pathVariables = route.match(exchange);
if (pathVariables == null)
continue;
exchange.getPathVariables().putAll(pathVariables);
for (RequestHandler handler : route.getHandlers()) {
response = handler.handle(exchange);
if (exchange.getMethod() == HTTPMethod.WEBSOCKET) {
Exchange.exchanges.remove();
return;
}
if (response != null)
break routes;
}
exchange.getPathVariables().clear();
}
exchange.getPathVariables().clear();
}
} catch (Throwable ex) {
response = exceptionHandler.handle(exchange, ex);
}
} catch (Throwable ex) {
response = exceptionHandler.handle(exchange, ex);
}
if (response == null)
response = notFoundHandler.handle(exchange);
exchange.getPathVariables().clear();
for (Route route : afterRoutes) {
Map<String, Object> pathVariables = route.match(exchange);
if (pathVariables == null)
continue;
exchange.getPathVariables().putAll(pathVariables);
for (AfterRequestHandler handler : route.getAfterHandlers())
response = handler.handleAfter(exchange, response);
if (response == null)
response = notFoundHandler.handle(exchange);
exchange.getPathVariables().clear();
for (Route route : afterRoutes) {
Map<String, Object> pathVariables = route.match(exchange);
if (pathVariables == null)
continue;
exchange.getPathVariables().putAll(pathVariables);
for (AfterRequestHandler handler : route.getAfterHandlers())
response = handler.handleAfter(exchange, response);
exchange.getPathVariables().clear();
}
if (response != null)
exchange.write(transformResponse(exchange, response));
if (exchange.getMethod() != HTTPMethod.WEBSOCKET)
exchange.close();
Exchange.exchanges.remove();
return;
} catch (Throwable ex) {
try {
exchange.write(transformResponse(exchange, exceptionHandler.handle(exchange, ex)));
} catch (Throwable ex2) {
exchange.status(500);
logger.log(Level.SEVERE, ex2, () -> "An error occured in the exception handler!");
}
}
if (response != null)
exchange.write(transformResponse(exchange, response));
if (exchange.getMethod() != HTTPMethod.WEBSOCKET)
exchange.close();
Exchange.exchanges.remove();
return;
} catch (Throwable ex) {
try {
exchange.write(transformResponse(exchange, exceptionHandler.handle(exchange, ex)));
} catch (Throwable ex2) {
logger.log(Level.SEVERE, ex2, () -> "An error occured in the exception handler!");
}
} catch (Exception ex) {
// This should never be reached, just added this as a precaution
logger.log(Level.SEVERE, ex, () -> "An unexpected error occured in the exception handling of the exception handler (probably while setting the status)");
}
Exchange.exchanges.remove();
exchange.close();
Expand Down Expand Up @@ -411,6 +421,11 @@ public ExceptionHandler getExceptionHandler() {
return exceptionHandler;
}

public HTTPRouter enableMultipart(PartContentCache cache) {
this.multipartContentCache = cache;
return this;
}

public boolean isFormMethods() {
return formMethods;
}
Expand Down
97 changes: 97 additions & 0 deletions src/main/java/org/javawebstack/http/router/multipart/Part.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package org.javawebstack.http.router.multipart;

import org.apache.commons.fileupload.MultipartStream;
import org.javawebstack.http.router.Exchange;
import org.javawebstack.http.router.multipart.content.PartContent;
import org.javawebstack.http.router.multipart.content.PartContentCache;
import org.javawebstack.http.router.util.HeaderValue;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Part {

final Map<String, String> headers;
final PartContent content;
String name;
String contentType;

public Part(Map<String, String> headers, PartContent content) {
this.headers = headers;
this.content = content;
if(headers.containsKey("content-disposition")) {
HeaderValue contentDisposition = new HeaderValue(headers.get("content-disposition"));
if(contentDisposition.getDirectives().containsKey("name"))
name = contentDisposition.getDirectives().get("name");
}
if(headers.containsKey("content-type")) {
contentType = new HeaderValue(headers.get("content-type")).getValue();
}
}

public static List<Part> parse(InputStream inputStream, byte[] boundary, PartContentCache cache) throws IOException {
MultipartStream stream = new MultipartStream(inputStream, boundary, 1024, null);
List<Part> parts = new ArrayList<>();
if(stream.skipPreamble()) {
do {
Map<String, String> headers = Stream.of(
stream.readHeaders().split("\r?\n")
)
.filter(l -> !l.trim().isEmpty())
.map(l -> l.split(": ", 2))
.collect(Collectors.toMap(
a -> a[0].toLowerCase(Locale.ROOT),
a -> a.length == 2 ? a[1] : ""
));
PartContent c = cache.store(os -> {
try {
stream.readBodyData(os);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
parts.add(new Part(headers, c));
} while (stream.readBoundary());
}
return parts;
}

public Map<String, String> getHeaders() {
return headers;
}

public InputStream getContentStream() {
return content.read();
}

public String getName() {
return name;
}

public String getContentType() {
return contentType;
}

public byte[] getContentBytes() {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream stream = getContentStream();
int b;
try {
while ((b = stream.read()) != -1)
baos.write(b);
stream.close();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
return baos.toByteArray();
}

public void discard() {
content.discard();
}

}
Loading

0 comments on commit 08fc77f

Please sign in to comment.