io.muserver.rest.RestHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mu-server Show documentation
Show all versions of mu-server Show documentation
A simple but powerful web server framework
package io.muserver.rest;
import io.muserver.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.NotAllowedException;
import javax.ws.rs.ServerErrorException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.*;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.sse.Sse;
import javax.ws.rs.sse.SseEventSink;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.*;
import java.util.concurrent.CompletionStage;
import static io.muserver.Mutils.hasValue;
import static io.muserver.rest.CORSConfig.getAllowedMethods;
import static io.muserver.rest.JaxRSResponse.muHeadersToJaxObj;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
/**
* A handler that serves JAX-RS resources.
*
* @see RestHandlerBuilder#restHandler(Object...)
*/
public class RestHandler implements MuHandler {
private static final Logger log = LoggerFactory.getLogger(RestHandler.class);
private final RequestMatcher requestMatcher;
private final EntityProviders entityProviders;
private final MuHandler documentor;
private final CustomExceptionMapper customExceptionMapper;
private final FilterManagerThing filterManagerThing;
private final CORSConfig corsConfig;
RestHandler(EntityProviders entityProviders, Set roots, MuHandler documentor, CustomExceptionMapper customExceptionMapper, FilterManagerThing filterManagerThing, CORSConfig corsConfig) {
this.requestMatcher = new RequestMatcher(roots);
this.entityProviders = entityProviders;
this.documentor = documentor;
this.customExceptionMapper = customExceptionMapper;
this.filterManagerThing = filterManagerThing;
this.corsConfig = corsConfig;
}
@Override
@SuppressWarnings("unchecked")
public boolean handle(MuRequest muRequest, MuResponse muResponse) throws Exception {
if (documentor != null && documentor.handle(muRequest, muResponse)) {
return true;
}
AsyncHandle asyncHandle = null;
List acceptHeaders;
try {
acceptHeaders = MediaTypeDeterminer.parseAcceptHeaders(muRequest.headers().getAll(HeaderNames.ACCEPT));
} catch (IllegalArgumentException e) {
muResponse.status(400);
muResponse.contentType("text/plain");
muResponse.write(e.getMessage());
return true;
}
List producesRef = null;
List directlyProducesRef = null;
SecurityContext securityContext = muRequest.uri().getScheme().equals("https") ? MuSecurityContext.notLoggedInHttpsContext : MuSecurityContext.notLoggedInHttpContext;
MuContainerRequestContext requestContext = new MuContainerRequestContext(muRequest, new LazyAccessInputStream(muRequest), Mutils.trim(muRequest.relativePath(), "/"), securityContext);
try {
filterManagerThing.onPreMatch(requestContext);
String relativePath = requestContext.getUriInfo().getPath(false);
String requestContentType = muRequest.headers().get(HeaderNames.CONTENT_TYPE);
RequestMatcher.MatchedMethod mm;
try {
mm = requestMatcher.findResourceMethod(requestContext.getMuMethod(), relativePath, acceptHeaders, requestContentType);
} catch (NotAllowedException e) {
if (requestContext.getMuMethod() == Method.HEAD) {
mm = requestMatcher.findResourceMethod(Method.GET, relativePath, acceptHeaders, requestContentType);
} else if (requestContext.getMuMethod() == Method.OPTIONS) {
Set matchedMethodsForPath = requestMatcher.getMatchedMethodsForPath(relativePath);
muResponse.headers().set(HeaderNames.ALLOW, getAllowedMethods(matchedMethodsForPath));
corsConfig.writeHeaders(muRequest, muResponse, matchedMethodsForPath);
return true;
} else {
throw e;
}
}
corsConfig.writeHeaders(muRequest, muResponse, Collections.singleton(mm));
List produces = producesRef = mm.resourceMethod.resourceClass.produces;
List directlyProduces = directlyProducesRef = mm.resourceMethod.directlyProduces;
ResourceMethod rm = mm.resourceMethod;
Object[] params = new Object[rm.methodHandle.getParameterCount()];
requestContext.setMatchedMethod(mm);
filterManagerThing.onPostMatch(requestContext);
boolean isAsync = false;
for (ResourceMethodParam param : rm.params) {
Object paramValue;
if (param.source == ResourceMethodParam.ValueSource.MESSAGE_BODY) {
if (requestContext.hasEntity()) {
paramValue = readRequestEntity(requestContentType, rm, param.parameterHandle, requestContext.getEntityStream(), entityProviders, requestContext.getHeaders());
} else {
throw new BadRequestException("No request body was sent to the " + param.parameterHandle.getName()
+ " parameter of the resource method \"" + rm.methodHandle + "\" "
+ "- if this should be optional then specify an @DefaultValue annotation on the parameter");
}
} else if (param.source == ResourceMethodParam.ValueSource.CONTEXT) {
paramValue = getContextParam(requestContext, muResponse, relativePath, mm, param, entityProviders);
} else if (param.source == ResourceMethodParam.ValueSource.SUSPENDED) {
if (isAsync) {
throw new MuException("A REST method can only have one @Suspended attribute. Error for " + rm);
}
isAsync = true;
asyncHandle = muRequest.handleAsync();
paramValue = new AsyncResponseAdapter(asyncHandle, response -> sendResponse(0, requestContext, muResponse, acceptHeaders, produces, directlyProduces, response));
} else {
ResourceMethodParam.RequestBasedParam rbp = (ResourceMethodParam.RequestBasedParam) param;
paramValue = rbp.getValue(muRequest, mm);
}
params[param.index] = paramValue;
}
Object result = rm.invoke(params);
isAsync |= muRequest.isAsync();
if (!isAsync) {
if (result instanceof CompletionStage) {
AsyncHandle asyncHandle1 = muRequest.handleAsync();
CompletionStage cs = (CompletionStage) result;
cs.thenAccept(o -> {
try {
sendResponse(0, requestContext, muResponse, acceptHeaders, produces, directlyProduces, o);
asyncHandle1.complete();
} catch (Exception e) {
asyncHandle1.complete(e);
}
});
} else {
sendResponse(0, requestContext, muResponse, acceptHeaders, produces, directlyProduces, result);
}
}
} catch (NotMatchedException e) {
return false;
} catch (Exception ex) {
if (ex instanceof WebApplicationException) {
dealWithWebApplicationException(0, requestContext, muResponse, (WebApplicationException) ex, acceptHeaders,
producesRef == null ? emptyList() : producesRef, directlyProducesRef == null ? emptyList() : directlyProducesRef);
} else {
dealWithUnhandledException(0, requestContext, muResponse, ex, acceptHeaders, producesRef, directlyProducesRef);
}
if (asyncHandle != null) {
asyncHandle.complete();
}
}
return true;
}
private void dealWithUnhandledException(int nestingLevel, MuContainerRequestContext request, MuResponse muResponse, Exception ex, List acceptHeaders, List producesRef, List directlyProducesRef) throws Exception {
Response response = customExceptionMapper.toResponse(ex);
if (response == null) {
throw ex;
}
sendResponse(nestingLevel, request, muResponse, acceptHeaders, producesRef, directlyProducesRef, response);
}
private void sendResponse(int nestingLevel, MuContainerRequestContext requestContext, MuResponse muResponse, List acceptHeaders, List produces, List directlyProduces, Object result) throws Exception {
try {
if (!muResponse.hasStartedSendingData()) {
ObjWithType obj = ObjWithType.objType(result);
if (obj.entity instanceof Exception) {
throw (Exception) obj.entity;
}
JaxRSResponse jaxRSResponse = obj.response;
if (jaxRSResponse == null) {
jaxRSResponse = new JaxRSResponse(Response.Status.fromStatusCode(obj.status()), new Headers(), obj.entity, null, new NewCookie[0]);
}
MuResponseContext responseContext = new MuResponseContext(jaxRSResponse, obj, requestContext.getMuMethod() == Method.HEAD ? NullOutputStream.INSTANCE : new LazyAccessOutputStream(muResponse));
if (obj.entity != null) {
Annotation[] annotations = new Annotation[0]; // TODO set this properly
MediaType responseMediaType = MediaTypeDeterminer.determine(obj, produces, directlyProduces, entityProviders.writers, acceptHeaders);
responseContext.setEntity(result, annotations, responseMediaType);
}
filterManagerThing.onBeforeSendResponse(requestContext, responseContext);
muResponse.status(responseContext.getStatus());
muResponse.headers().setAll(responseContext.muHeaders());
for (NewCookie cookie : jaxRSResponse.getCookies().values()) {
muResponse.headers().add(HeaderNames.SET_COOKIE, cookie.toString());
}
Object entity = responseContext.getEntity();
if (entity == null) {
muResponse.headers().set(HeaderNames.CONTENT_LENGTH, 0);
} else {
MediaType responseMediaType = responseContext.getMediaType();
Annotation[] entityAnnotations = responseContext.getEntityAnnotations();
Class entityType = responseContext.getEntityClass();
Type entityGenericType = responseContext.getEntityType();
MessageBodyWriter messageBodyWriter = entityProviders.selectWriter(entityType, entityGenericType, entityAnnotations, responseMediaType);
long size = messageBodyWriter.getSize(entity, entityType, entityGenericType, entityAnnotations, responseMediaType);
if (size > -1) {
muResponse.headers().set(HeaderNames.CONTENT_LENGTH.toString(), size);
}
muResponse.headers().set(HeaderNames.CONTENT_TYPE, responseMediaType.toString());
messageBodyWriter.writeTo(entity, entityType, entityGenericType, entityAnnotations, responseMediaType, muHeadersToJaxObj(muResponse.headers()), responseContext.getEntityStream());
}
}
} catch (WebApplicationException e) {
dealWithWebApplicationException(nestingLevel + 1, requestContext, muResponse, e, acceptHeaders, produces, directlyProduces);
} catch (Exception ex) {
dealWithUnhandledException(nestingLevel + 1, requestContext, muResponse, ex, acceptHeaders, produces, directlyProduces);
}
}
private void dealWithWebApplicationException(int nestingLevel, MuContainerRequestContext requestContext, MuResponse muResponse, WebApplicationException e, List acceptHeaders, List produces, List directlyProduces) throws Exception {
if (muResponse.hasStartedSendingData()) {
log.warn("A web application exception " + e + " was thrown for " + requestContext.muRequest + ", however the response code and message cannot be sent to the client as some data was already sent.");
} else {
Response r = e.getResponse();
if (nestingLevel < 2) {
Response.ResponseBuilder toSend = Response.fromResponse(r);
if (r.getEntity() == null) {
toSend.type(MediaType.TEXT_HTML_TYPE);
String entity = "" + r.getStatus() + " " + r.getStatusInfo().getReasonPhrase() + "
";
if (e instanceof ServerErrorException) {
String errorID = "ERR-" + UUID.randomUUID().toString();
log.info("Sending a 500 to the client with ErrorID=" + errorID + " for " + requestContext.muRequest, e);
toSend.entity(entity + "ErrorID=" + errorID + "
");
} else {
toSend.entity(entity + "" + Mutils.htmlEncode(e.getMessage()) + "
");
}
}
sendResponse(nestingLevel + 1, requestContext, muResponse, acceptHeaders, produces, directlyProduces, toSend.build());
} else {
muResponse.status(r.getStatus());
muResponse.contentType(ContentTypes.TEXT_PLAIN);
Response.StatusType statusInfo = r.getStatusInfo();
String message = statusInfo.getStatusCode() + " " + statusInfo.getReasonPhrase() + " - " + e.getMessage();
muResponse.write(message);
}
}
}
private static Object getContextParam(MuContainerRequestContext requestContext, MuResponse muResponse, String relativePath, RequestMatcher.MatchedMethod mm, ResourceMethodParam param, EntityProviders providers) {
MuRequest request = requestContext.muRequest;
Object paramValue;
Class> type = param.parameterHandle.getType();
if (type.equals(UriInfo.class)) {
paramValue = createUriInfo(relativePath, mm, request.uri().resolve(request.contextPath() + "/"), request.uri());
} else if (type.equals(MuResponse.class)) {
paramValue = muResponse;
} else if (type.equals(MuRequest.class)) {
paramValue = request;
} else if (type.equals(HttpHeaders.class)) {
paramValue = new JaxRsHttpHeadersAdapter(request.headers(), request.cookies());
} else if (type.equals(SecurityContext.class)) {
return requestContext.getSecurityContext();
} else if (type.equals(Sse.class)) {
return new JaxSseImpl();
} else if (type.equals(SseEventSink.class)) {
AsyncSsePublisher pub = AsyncSsePublisher.start(requestContext.muRequest, muResponse);
return new JaxSseEventSinkImpl(pub, muResponse, providers);
} else {
throw new ServerErrorException("MuServer does not support @Context parameters with type " + type, 500);
}
return paramValue;
}
static MuUriInfo createUriInfo(String relativePath, RequestMatcher.MatchedMethod mm, URI baseUri, URI requestUri) {
List matchedURIs = new ArrayList<>();
matchedURIs.add(relativePath);
ResourceMethod rm = null;
if (mm != null) {
String methodSpecific = mm.pathMatch.regexMatcher().group();
matchedURIs.add(relativePath.replace("/" + methodSpecific, ""));
rm = mm.resourceMethod;
}
List