All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jboss.resteasy.reactive.server.handlers.ClassRoutingHandler Maven / Gradle / Ivy

There is a newer version: 3.17.5
Show newest version
package org.jboss.resteasy.reactive.server.handlers;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jakarta.ws.rs.HttpMethod;
import jakarta.ws.rs.NotAcceptableException;
import jakarta.ws.rs.NotAllowedException;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.NotSupportedException;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;

import org.jboss.resteasy.reactive.common.headers.MediaTypeHeaderDelegate;
import org.jboss.resteasy.reactive.common.util.MediaTypeHelper;
import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
import org.jboss.resteasy.reactive.server.jaxrs.ProvidersImpl;
import org.jboss.resteasy.reactive.server.jaxrs.ResponseBuilderImpl;
import org.jboss.resteasy.reactive.server.mapping.RequestMapper;
import org.jboss.resteasy.reactive.server.mapping.RuntimeResource;
import org.jboss.resteasy.reactive.server.spi.ServerRestHandler;

@SuppressWarnings("ForLoopReplaceableByForEach")
public class ClassRoutingHandler implements ServerRestHandler {
    private static final String INVALID_ACCEPT_HEADER_MESSAGE = "The accept header value did not match the value in @Produces";

    private final Map> mappers;
    private final int parameterOffset;
    final boolean servletPresent;

    public ClassRoutingHandler(Map> mappers, int parameterOffset,
            boolean servletPresent) {
        this.mappers = mappers;
        this.parameterOffset = parameterOffset;
        this.servletPresent = servletPresent;
    }

    @Override
    public void handle(ResteasyReactiveRequestContext requestContext) throws Exception {
        RequestMapper mapper = mappers.get(requestContext.getMethod());
        if (mapper == null) {
            String requestMethod = requestContext.getMethod();
            if (requestMethod.equals(HttpMethod.HEAD)) {
                mapper = mappers.get(HttpMethod.GET);
            } else if (requestMethod.equals(HttpMethod.OPTIONS)) {
                Set allowedMethods = new HashSet<>();
                for (String method : mappers.keySet()) {
                    if (method == null) {
                        continue;
                    }
                    allowedMethods.add(method);
                }
                allowedMethods.add(HttpMethod.OPTIONS);
                allowedMethods.add(HttpMethod.HEAD);
                requestContext.abortWith(Response.ok().allow(allowedMethods).build());
                return;
            }
            if (mapper == null) {
                mapper = mappers.get(null);
            }
            if (mapper == null) {
                // The idea here is to check if any of the mappers of the class could map the request - if the HTTP Method were correct
                String remaining = getRemaining(requestContext);
                for (RequestMapper existingMapper : mappers.values()) {
                    if (existingMapper.map(remaining) != null) {
                        throw new NotAllowedException(
                                new ResponseBuilderImpl().status(Response.Status.METHOD_NOT_ALLOWED).build());
                    }
                }
                throwNotFound(requestContext);
                return;
            }
        }
        String remaining = getRemaining(requestContext);
        RequestMapper.RequestMatch target = mapper.map(remaining);
        if (target == null) {
            if (requestContext.getMethod().equals(HttpMethod.HEAD)) {
                mapper = mappers.get(HttpMethod.GET);
                if (mapper != null) {
                    target = mapper.map(remaining);
                }
            }

            if (target == null) {
                // The idea here is to check if any of the mappers of the class could map the request - if the HTTP Method were correct
                for (Map.Entry> entry : mappers.entrySet()) {
                    if (entry.getKey() == null) {
                        continue;
                    }
                    if (entry.getKey().equals(requestContext.getMethod())) {
                        continue;
                    }
                    if (entry.getValue().map(remaining) != null) {
                        throw new NotAllowedException(
                                new ResponseBuilderImpl().status(Response.Status.METHOD_NOT_ALLOWED).build());
                    }
                }
                throwNotFound(requestContext);
                return;
            }
        }

        // according to the spec we need to return HTTP 415 when content-type header doesn't match what is specified in @Consumes
        if (!target.value.getConsumes().isEmpty()) {
            String contentType = (String) requestContext.getHeader(HttpHeaders.CONTENT_TYPE, true);
            if (contentType != null) {
                try {
                    if (MediaTypeHelper.getFirstMatch(
                            target.value.getConsumes(),
                            Collections.singletonList(MediaType.valueOf(contentType))) == null) {
                        throw new NotSupportedException("The content-type header value did not match the value in @Consumes");
                    }
                } catch (IllegalArgumentException e) {
                    throw new NotSupportedException("The content-type header value did not correspond to a valid media type");
                }
            }
        }
        // according to the spec we need to return HTTP 406 when Accept header doesn't match what is specified in @Produces
        if (target.value.getProduces() != null) {
            // there could potentially be multiple Accept headers and we need to response with 406
            // if none match the method's @Produces
            List accepts = (List) requestContext.getHeader(HttpHeaders.ACCEPT, false);
            if (!accepts.isEmpty()) {
                boolean hasAtLeastOneMatch = false;
                for (int i = 0; i < accepts.size(); i++) {
                    try {
                        boolean matches = acceptHeaderMatches(target, accepts.get(i));
                        if (matches) {
                            hasAtLeastOneMatch = true;
                            break;
                        }
                    } catch (IllegalArgumentException ignored) {
                        // the provided header was not valid
                    }
                }
                if (!hasAtLeastOneMatch) {
                    throw new NotAcceptableException(INVALID_ACCEPT_HEADER_MESSAGE);
                }
            }

            requestContext.setProducesChecked(true);
        }

        requestContext.restart(target.value);
        requestContext.setRemaining(target.remaining);
        for (int i = 0; i < target.pathParamValues.length; ++i) {
            String pathParamValue = target.pathParamValues[i];
            if (pathParamValue == null) {
                break;
            }
            requestContext.setPathParamValue(i + parameterOffset, pathParamValue);
        }
    }

    /**
     * @return {@code true} if the provided string matches one of the {@code @Produces} values of the resource method
     * @throws IllegalArgumentException if the provided string cannot be parsed into a {@link MediaType}
     */
    private boolean acceptHeaderMatches(RequestMapper.RequestMatch target, String accepts) {
        if ((accepts != null) && !accepts.equals(MediaType.WILDCARD)) {
            int commaIndex = accepts.indexOf(',');
            boolean multipleAcceptsValues = commaIndex >= 0;
            MediaType[] producesMediaTypes = target.value.getProduces().getSortedOriginalMediaTypes();
            if (!multipleAcceptsValues && (producesMediaTypes.length == 1)) {
                // the point of this branch is to eliminate any list creation or string indexing as none is needed
                MediaType providedMediaType = producesMediaTypes[0];
                return providedMediaType.isCompatible(toMediaType(accepts.trim()));
            } else if (multipleAcceptsValues && (producesMediaTypes.length == 1)) {
                // this is fairly common case, so we want it to be as fast as possible
                // we do that by manually splitting the accepts header and immediately checking
                // if the value is compatible with the produces media type
                boolean compatible = false;
                int begin = 0;

                do {
                    String acceptPart;
                    if (commaIndex == -1) { // this is the case where we are checking the remainder of the string
                        acceptPart = accepts.substring(begin);
                    } else {
                        acceptPart = accepts.substring(begin, commaIndex);
                    }
                    if (producesMediaTypes[0].isCompatible(toMediaType(acceptPart.trim()))) {
                        compatible = true;
                        break;
                    } else if (commaIndex == -1) { // we have reached the end and not found any compatible media types
                        break;
                    }
                    begin = commaIndex + 1; // the next part will start at the character after the comma
                    if (begin >= (accepts.length() - 1)) { // if we have reached this point, then are no compatible media types
                        break;
                    }
                    commaIndex = accepts.indexOf(',', begin);
                } while (true);

                return compatible;
            } else {
                // don't use any of the JAX-RS stuff from the various MediaType helper as we want to be as performant as possible
                List acceptsMediaTypes;
                if (accepts.contains(",")) {
                    String[] parts = accepts.split(",");
                    acceptsMediaTypes = new ArrayList<>(parts.length);
                    for (int i = 0; i < parts.length; i++) {
                        String part = parts[i];
                        acceptsMediaTypes.add(toMediaType(part.trim()));
                    }
                } else {
                    acceptsMediaTypes = Collections.singletonList(toMediaType(accepts));
                }
                return MediaTypeHelper.getFirstMatch(Arrays.asList(producesMediaTypes),
                        acceptsMediaTypes) != null;
            }
        }

        return true;
    }

    private MediaType toMediaType(String mediaTypeStr) {
        return MediaTypeHeaderDelegate.parse(mediaTypeStr);
    }

    private void throwNotFound(ResteasyReactiveRequestContext requestContext) {
        ProvidersImpl providers = requestContext.getProviders();
        ExceptionMapper exceptionMapper = providers.getExceptionMapper(NotFoundException.class);

        if (exceptionMapper == null || servletPresent) {
            if (requestContext.resumeExternalProcessing()) {
                return;
            }
        }
        // the exception mapper needs access to request scoped beans, so make sure we have the context
        requestContext.requireCDIRequestScope();
        throw new NotFoundException("Unable to find matching target resource method");

    }

    private String getRemaining(ResteasyReactiveRequestContext requestContext) {
        return requestContext.getRemaining().isEmpty() ? "/" : requestContext.getRemaining();
    }

    public Map> getMappers() {
        return mappers;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy