
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
The newest version!
package io.muserver.rest;
import io.muserver.*;
import jakarta.ws.rs.ClientErrorException;
import jakarta.ws.rs.NotAllowedException;
import jakarta.ws.rs.ServerErrorException;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.*;
import jakarta.ws.rs.ext.MessageBodyWriter;
import jakarta.ws.rs.ext.ParamConverterProvider;
import jakarta.ws.rs.ext.ReaderInterceptor;
import jakarta.ws.rs.ext.WriterInterceptor;
import jakarta.ws.rs.sse.Sse;
import jakarta.ws.rs.sse.SseEventSink;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 java.util.function.Function;
import static io.muserver.rest.CORSConfig.getAllowedMethods;
import static java.util.Collections.emptyList;
/**
* 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;
private final List paramConverterProviders;
private final SchemaObjectCustomizer schemaObjectCustomizer;
private final List readerInterceptors;
private final List writerInterceptors;
private final CollectionParameterStrategy collectionParameterStrategy;
RestHandler(EntityProviders entityProviders, List roots, MuHandler documentor, CustomExceptionMapper customExceptionMapper, FilterManagerThing filterManagerThing, CORSConfig corsConfig, List paramConverterProviders, SchemaObjectCustomizer schemaObjectCustomizer, List readerInterceptors, List writerInterceptors, CollectionParameterStrategy collectionParameterStrategy) {
this.requestMatcher = new RequestMatcher(roots);
this.entityProviders = entityProviders;
this.documentor = documentor;
this.customExceptionMapper = customExceptionMapper;
this.filterManagerThing = filterManagerThing;
this.corsConfig = corsConfig;
this.paramConverterProviders = paramConverterProviders;
this.schemaObjectCustomizer = schemaObjectCustomizer;
this.readerInterceptors = readerInterceptors;
this.writerInterceptors = writerInterceptors;
this.collectionParameterStrategy = collectionParameterStrategy;
}
@Override
@SuppressWarnings("unchecked")
public boolean handle(MuRequest muRequest, MuResponse muResponse) throws Exception {
if (documentor != null && documentor.handle(muRequest, muResponse)) {
return true;
}
List acceptHeaders;
try {
acceptHeaders = MediaTypeDeterminer.parseAcceptHeaders(muRequest.headers().getAll(HeaderNames.ACCEPT));
} catch (IllegalArgumentException e) {
throw new ClientErrorException(e.getMessage(), 400);
}
List producesRef = null;
List directlyProducesRef = null;
SecurityContext securityContext = muRequest.uri().getScheme().equals("https") ? MuSecurityContext.notLoggedInHttpsContext : MuSecurityContext.notLoggedInHttpContext;
JaxRSRequest requestContext = new JaxRSRequest(muRequest, muResponse, new LazyAccessInputStream(muRequest), Mutils.trim(muRequest.relativePath(), "/"), securityContext, readerInterceptors, entityProviders);
try {
filterManagerThing.onPreMatch(requestContext);
Function subResourceLocator = matchedMethod -> {
Function onSuspended = resourceMethod -> {
throw new MuException("Suspended is not supported on sub-resource locators. Method: " + resourceMethod.methodHandle);
};
ResourceMethod rm = matchedMethod.resourceMethod;
try {
Object instance = invokeResourceMethod(requestContext, muResponse, matchedMethod, onSuspended, entityProviders, collectionParameterStrategy);
return ResourceClass.forSubResourceLocator(rm, instance.getClass(), instance, schemaObjectCustomizer, paramConverterProviders);
} catch (WebApplicationException wae) {
throw wae;
} catch (Exception e) {
throw new MuException("Error creating instance returned by sub-resource-locator " + rm.methodHandle, e);
}
};
RequestMatcher.MatchedMethod mm;
try {
mm = requestMatcher.findResourceMethod(requestContext, requestContext.getMuMethod(), acceptHeaders, subResourceLocator);
} catch (NotAllowedException e) {
if (requestContext.getMuMethod() == Method.HEAD) {
mm = requestMatcher.findResourceMethod(requestContext, Method.GET, acceptHeaders, subResourceLocator);
} else if (requestContext.getMuMethod() == Method.OPTIONS) {
Set matchedMethodsForPath = requestMatcher.getMatchedMethodsForPath(requestContext.relativePath(), subResourceLocator);
muResponse.headers().set(HeaderNames.ALLOW, getAllowedMethods(matchedMethodsForPath));
corsConfig.writeHeadersInternal(muRequest, muResponse, matchedMethodsForPath);
return true;
} else {
throw e;
}
}
corsConfig.writeHeadersInternal(muRequest, muResponse, Collections.singleton(mm));
requestContext.setMatchedMethod(mm);
List produces = producesRef = mm.resourceMethod.resourceClass.produces;
List directlyProduces = directlyProducesRef = mm.resourceMethod.directlyProduces;
Annotation[] methodAnnotations = mm.resourceMethod.methodAnnotations;
filterManagerThing.onPostMatch(requestContext);
Function suspendedParamCallback = rm -> {
if (muRequest.isAsync()) {
throw new MuException("A REST method can only have one @Suspended attribute. Error for " + rm);
}
return new AsyncResponseAdapter(muRequest.handleAsync(),
response -> sendResponse(0, requestContext, muResponse, acceptHeaders, produces, directlyProduces, methodAnnotations, response));
};
Object result = invokeResourceMethod(requestContext, muResponse, mm, suspendedParamCallback, entityProviders, collectionParameterStrategy);
if (!muRequest.isAsync()) {
if (result instanceof CompletionStage) {
AsyncHandle asyncHandle1 = muRequest.handleAsync();
CompletionStage cs = (CompletionStage) result;
cs.thenAccept(o -> {
try {
sendResponse(0, requestContext, muResponse, acceptHeaders, produces, directlyProduces, methodAnnotations, o);
asyncHandle1.complete();
} catch (Exception e) {
asyncHandle1.complete(e);
}
});
} else {
sendResponse(0, requestContext, muResponse, acceptHeaders, produces, directlyProduces, methodAnnotations, result);
}
}
} catch (NotMatchedException e) {
return false;
} catch (Exception ex) {
if (producesRef == null) producesRef = emptyList();
if (directlyProducesRef == null) directlyProducesRef = emptyList();
dealWithUnhandledException(0, requestContext, muResponse, ex, acceptHeaders, producesRef, directlyProducesRef);
if (muRequest.isAsync()) {
muRequest.handleAsync().complete();
}
}
return true;
}
static Object invokeResourceMethod(JaxRSRequest requestContext, MuResponse muResponse, RequestMatcher.MatchedMethod mm, Function suspendedParamCallback, EntityProviders entityProviders, CollectionParameterStrategy collectionParameterStrategy) throws Exception {
ResourceMethod rm = mm.resourceMethod;
Object[] params = new Object[rm.methodHandle.getParameterCount()];
for (ResourceMethodParam param : rm.params) {
Object paramValue;
if (param.source == ResourceMethodParam.ValueSource.MESSAGE_BODY) {
paramValue = readRequestEntity(requestContext, param.parameterHandle);
} else if (param.source == ResourceMethodParam.ValueSource.CONTEXT) {
paramValue = getContextParam(requestContext, muResponse, mm, param, entityProviders);
} else if (param.source == ResourceMethodParam.ValueSource.SUSPENDED) {
paramValue = suspendedParamCallback.apply(rm);
} else {
ResourceMethodParam.RequestBasedParam rbp = (ResourceMethodParam.RequestBasedParam) param;
paramValue = rbp.getValue(requestContext, mm, collectionParameterStrategy);
}
params[param.index] = paramValue;
}
return rm.invoke(params);
}
private void dealWithUnhandledException(int nestingLevel, JaxRSRequest request, MuResponse muResponse, Exception ex, List acceptHeaders, List producesRef, List directlyProducesRef) throws Exception {
Response response = customExceptionMapper.toResponse(ex);
if (response == null && ex instanceof WebApplicationException) {
dealWithWebApplicationException(nestingLevel, request, muResponse, (WebApplicationException) ex, acceptHeaders, producesRef == null ? emptyList() : producesRef, directlyProducesRef == null ? emptyList() : directlyProducesRef);
} else if (response == null) {
throw ex;
} else {
sendResponse(nestingLevel, request, muResponse, acceptHeaders, producesRef, directlyProducesRef, JaxRSResponse.Builder.EMPTY_ANNOTATIONS, response);
}
}
private void sendResponse(int nestingLevel, JaxRSRequest requestContext, MuResponse muResponse, List acceptHeaders, List produces, List directlyProduces, Annotation[] annotations, Object result) throws Exception {
try {
if (requestContext.hasEntity()) {
requestContext.getEntityStream().close();
}
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 LowercasedMultivaluedHashMap<>(), obj, new NewCookie[0], emptyList(), JaxRSResponse.Builder.EMPTY_ANNOTATIONS);
}
try (LazyAccessOutputStream out = new LazyAccessOutputStream(muResponse)) {
jaxRSResponse.setEntityStream(requestContext.getMuMethod() == Method.HEAD ? NullOutputStream.INSTANCE : out);
jaxRSResponse.setRequestContext(requestContext);
Annotation[] writerAnnontations = annotations;
if (jaxRSResponse.getAnnotations().length > 0) {
if (writerAnnontations.length == 0) {
writerAnnontations = jaxRSResponse.getAnnotations();
} else {
writerAnnontations = Arrays.copyOf(annotations, annotations.length + jaxRSResponse.getAnnotations().length);
System.arraycopy(jaxRSResponse.getAnnotations(), 0, writerAnnontations, annotations.length, jaxRSResponse.getAnnotations().length);
}
}
if (obj.entity != null) {
MediaType responseMediaType = MediaTypeDeterminer.determine(obj, produces, directlyProduces, entityProviders.writers, acceptHeaders, writerAnnontations);
jaxRSResponse.setMediaType(responseMediaType);
}
filterManagerThing.onBeforeSendResponse(requestContext, jaxRSResponse);
if (jaxRSResponse.hasEntity()) {
jaxRSResponse.executeInterceptors(writerInterceptors); // run the interceptors
}
Object entity = jaxRSResponse.getEntity();
if (entity instanceof Exception) {
throw (Exception) entity;
}
int status = jaxRSResponse.getStatus();
muResponse.status(status);
boolean isHttp1 = requestContext.muRequest.protocol().equals("HTTP/1.1");
if (entity == null) {
if (status != 204 && status != 304 && status != 205) {
jaxRSResponse.getHeaders().putSingle("content-length", "0");
}
MuRuntimeDelegate.writeResponseHeaders(requestContext.muRequest.uri(), jaxRSResponse, muResponse, isHttp1);
} else {
MediaType responseMediaType = jaxRSResponse.getMediaType();
Class entityType = jaxRSResponse.getEntityClass();
Type entityGenericType = jaxRSResponse.getEntityType();
MessageBodyWriter messageBodyWriter = entityProviders.selectWriter(entityType, entityGenericType, writerAnnontations, responseMediaType);
long size = messageBodyWriter.getSize(entity, entityType, entityGenericType, writerAnnontations, responseMediaType);
if (size > -1) {
jaxRSResponse.getHeaders().putSingle("content-length", size);
}
String contentType = responseMediaType.toString();
if (responseMediaType.getType().equals("text") && !responseMediaType.getParameters().containsKey("charset")) {
contentType += ";charset=utf-8";
}
jaxRSResponse.getHeaders().putSingle("content-type", contentType);
MuRuntimeDelegate.writeResponseHeaders(requestContext.muRequest.uri(), jaxRSResponse, muResponse, isHttp1);
try {
messageBodyWriter.writeTo(jaxRSResponse.getEntity(), jaxRSResponse.getType(), jaxRSResponse.getGenericType(), writerAnnontations,
jaxRSResponse.getMediaType(), jaxRSResponse.getHeaders(), jaxRSResponse.getOutputStream());
} catch (Exception e) {
// remove the added headers before rewriting
if (!muResponse.hasStartedSendingData()) {
for (String added : jaxRSResponse.getHeaders().keySet()) {
muResponse.headers().remove(added);
}
}
throw e;
}
}
}
}
} catch (Exception ex) {
dealWithUnhandledException(nestingLevel + 1, requestContext, muResponse, ex, acceptHeaders, produces, directlyProduces);
}
}
private void dealWithWebApplicationException(int nestingLevel, JaxRSRequest 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, JaxRSResponse.Builder.EMPTY_ANNOTATIONS, toSend.build());
} else {
muResponse.status(r.getStatus());
muResponse.contentType(ContentTypes.TEXT_PLAIN_UTF8);
Response.StatusType statusInfo = r.getStatusInfo();
String message = statusInfo.getStatusCode() + " " + statusInfo.getReasonPhrase() + " - " + e.getMessage();
muResponse.write(message);
}
}
}
private static Object getContextParam(JaxRSRequest requestContext, MuResponse muResponse, 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(requestContext.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 (SecurityContext.class.isAssignableFrom(type)) {
SecurityContext sc = requestContext.getSecurityContext();
if (sc != null && !type.isAssignableFrom(sc.getClass())) {
throw new MuException("Invalid security context type: " + sc.getClass() + " being used for " + mm.resourceMethod.methodHandle);
}
return sc;
} 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 if (type.equals(ContainerRequestContext.class) || type.equals(Request.class)) {
return requestContext;
} 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) {
return new MuUriInfo(baseUri, requestUri, Mutils.trim(relativePath, "/"), mm);
}
private static Object readRequestEntity(JaxRSRequest requestContext, Parameter parameter) throws java.io.IOException {
requestContext.setAnnotations(parameter.getDeclaredAnnotations());
requestContext.setType(parameter.getType());
requestContext.setGenericType(parameter.getParameterizedType());
return requestContext.executeInterceptors();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy