/** * Construct HttpResourceHandler. Reads all annotations from all the handler classes and methods * passed in, constructs patternPathRouter which is routable by path to {@code HttpResourceModel} * as destination of the route. * * @param handlers Iterable of HttpHandler. * @param interceptors Iterable of interceptors. * @param urlRewriter URL re-writer. * @param exceptionHandler Exception handler */ public MicroserviceMetadata( Iterable<? extends Object> handlers, Iterable<? extends Interceptor> interceptors, URLRewriter urlRewriter, ExceptionHandler exceptionHandler) { // Store the handlers to call init and destroy on all handlers. this.handlers = ImmutableList.copyOf(handlers); this.interceptors = ImmutableList.copyOf(interceptors); this.urlRewriter = urlRewriter; for (Object handler : handlers) { String basePath = ""; if (handler.getClass().isAnnotationPresent(Path.class)) { basePath = handler.getClass().getAnnotation(Path.class).value(); } for (Method method : handler.getClass().getDeclaredMethods()) { if (method.isAnnotationPresent(PostConstruct.class) || method.isAnnotationPresent(PreDestroy.class)) { continue; } if (Modifier.isPublic(method.getModifiers()) && isHttpMethodAvailable(method)) { String relativePath = ""; if (method.getAnnotation(Path.class) != null) { relativePath = method.getAnnotation(Path.class).value(); } String absolutePath = String.format("%s/%s", basePath, relativePath); patternRouter.add( absolutePath, new HttpResourceModel(absolutePath, method, handler, new ExceptionHandler())); } else { log.trace( "Not adding method {}({}) to path routing like. " + "HTTP calls will not be routed to this method", method.getName(), method.getParameterTypes()); } } } }
/** * MicroserviceMetadata handles the http request. HttpResourceHandler looks up all Jax-rs * annotations in classes and dispatches to appropriate method on receiving requests. */ public final class MicroserviceMetadata { private static final Logger log = LoggerFactory.getLogger(MicroserviceMetadata.class); private final PatternPathRouterWithGroups<HttpResourceModel> patternRouter = PatternPathRouterWithGroups.create(); private final Iterable<Object> handlers; private final Iterable<Interceptor> interceptors; private final URLRewriter urlRewriter; /** * Construct HttpResourceHandler. Reads all annotations from all the handler classes and methods * passed in, constructs patternPathRouter which is routable by path to {@code HttpResourceModel} * as destination of the route. * * @param handlers Iterable of HttpHandler. * @param interceptors Iterable of interceptors. * @param urlRewriter URL re-writer. * @param exceptionHandler Exception handler */ public MicroserviceMetadata( Iterable<? extends Object> handlers, Iterable<? extends Interceptor> interceptors, URLRewriter urlRewriter, ExceptionHandler exceptionHandler) { // Store the handlers to call init and destroy on all handlers. this.handlers = ImmutableList.copyOf(handlers); this.interceptors = ImmutableList.copyOf(interceptors); this.urlRewriter = urlRewriter; for (Object handler : handlers) { String basePath = ""; if (handler.getClass().isAnnotationPresent(Path.class)) { basePath = handler.getClass().getAnnotation(Path.class).value(); } for (Method method : handler.getClass().getDeclaredMethods()) { if (method.isAnnotationPresent(PostConstruct.class) || method.isAnnotationPresent(PreDestroy.class)) { continue; } if (Modifier.isPublic(method.getModifiers()) && isHttpMethodAvailable(method)) { String relativePath = ""; if (method.getAnnotation(Path.class) != null) { relativePath = method.getAnnotation(Path.class).value(); } String absolutePath = String.format("%s/%s", basePath, relativePath); patternRouter.add( absolutePath, new HttpResourceModel(absolutePath, method, handler, new ExceptionHandler())); } else { log.trace( "Not adding method {}({}) to path routing like. " + "HTTP calls will not be routed to this method", method.getName(), method.getParameterTypes()); } } } } private boolean isHttpMethodAvailable(Method method) { return method.isAnnotationPresent(GET.class) || method.isAnnotationPresent(PUT.class) || method.isAnnotationPresent(POST.class) || method.isAnnotationPresent(DELETE.class); } /** * Call the appropriate handler for handling the httprequest. 404 if path is not found. 405 if * path is found but httpMethod does not match what's configured. * * @param request instance of {@code HttpRequest} * @param responder instance of {@code HttpResponder} to handle the request. * @return HttpMethodInfo object, null if urlRewriter rewrite returns false, also when method * cannot be invoked. * @throws HandlerException If URL rewriting fails */ public HttpMethodInfoBuilder getDestinationMethod(HttpRequest request, HttpResponder responder) throws HandlerException { if (urlRewriter != null) { try { request.setUri(URI.create(request.getUri()).normalize().toString()); if (!urlRewriter.rewrite(request, responder)) { return null; } } catch (Throwable t) { log.error("Exception thrown during rewriting of uri {}", request.getUri(), t); throw new HandlerException( HttpResponseStatus.INTERNAL_SERVER_ERROR, String.format("Caught exception processing request. Reason: %s", t.getMessage())); } } String acceptHeaderStr = request.headers().get(HttpHeaders.Names.ACCEPT); List<String> acceptHeader = (acceptHeaderStr != null) ? Arrays.asList(acceptHeaderStr.split("\\s*,\\s*")) .stream() .map(mediaType -> mediaType.split("\\s*;\\s*")[0]) .collect(Collectors.toList()) : null; String contentTypeHeaderStr = request.headers().get(HttpHeaders.Names.CONTENT_TYPE); // Trim specified charset since UTF-8 is assumed String contentTypeHeader = (contentTypeHeaderStr != null) ? contentTypeHeaderStr.split("\\s*;\\s*")[0] : null; try { String path = URI.create(request.getUri()).normalize().getPath(); List<PatternPathRouterWithGroups.RoutableDestination<HttpResourceModel>> routableDestinations = patternRouter.getDestinations(path); List<PatternPathRouterWithGroups.RoutableDestination<HttpResourceModel>> matchedDestinations = getMatchedDestination(routableDestinations, request.getMethod(), path); if (!matchedDestinations.isEmpty()) { PatternPathRouterWithGroups.RoutableDestination<HttpResourceModel> matchedDestination = matchedDestinations .stream() .filter( matchedDestination1 -> { return matchedDestination1 .getDestination() .matchConsumeMediaType(contentTypeHeader) && matchedDestination1 .getDestination() .matchProduceMediaType(acceptHeader); }) .findFirst() .get(); HttpResourceModel httpResourceModel = matchedDestination.getDestination(); // Call preCall method of handler interceptors. boolean terminated = false; ServiceMethodInfo serviceMethodInfo = new ServiceMethodInfo( httpResourceModel.getMethod().getDeclaringClass().getName(), httpResourceModel.getMethod()); for (Interceptor interceptor : interceptors) { if (!interceptor.preCall(request, responder, serviceMethodInfo)) { // Terminate further request processing if preCall returns false. terminated = true; break; } } // Call httpresource handle method, return the HttpMethodInfo Object. if (!terminated) { // Wrap responder to make post hook calls. responder = new WrappedHttpResponder(responder, interceptors, request, serviceMethodInfo); return HttpMethodInfoBuilder.getInstance() .httpResourceModel(httpResourceModel) .httpRequest(request) .httpResponder(responder) .requestInfo( matchedDestination.getGroupNameValues(), contentTypeHeader, acceptHeader); } } else if (!routableDestinations.isEmpty()) { // Found a matching resource but could not find the right HttpMethod so return 405 throw new HandlerException(HttpResponseStatus.METHOD_NOT_ALLOWED, request.getUri()); } else { throw new HandlerException( HttpResponseStatus.NOT_FOUND, String.format("Problem accessing: %s. Reason: Not Found", request.getUri())); } } catch (NoSuchElementException ex) { throw new HandlerException( HttpResponseStatus.UNSUPPORTED_MEDIA_TYPE, String.format("Problem accessing: %s. Reason: Unsupported Media Type", request.getUri()), ex); } return null; } /** * Get HttpResourceModel which matches the HttpMethod of the request. * * @param routableDestinations List of ResourceModels. * @param targetHttpMethod HttpMethod. * @param requestUri request URI. * @return RoutableDestination that matches httpMethod that needs to be handled. null if there are * no matches. */ private List<PatternPathRouterWithGroups.RoutableDestination<HttpResourceModel>> getMatchedDestination( List<PatternPathRouterWithGroups.RoutableDestination<HttpResourceModel>> routableDestinations, HttpMethod targetHttpMethod, String requestUri) { Iterable<String> requestUriParts = Splitter.on('/').omitEmptyStrings().split(requestUri); List<PatternPathRouterWithGroups.RoutableDestination<HttpResourceModel>> matchedDestinations = Lists.newArrayListWithExpectedSize(routableDestinations.size()); int maxExactMatch = 0; int maxGroupMatch = 0; int maxPatternLength = 0; for (PatternPathRouterWithGroups.RoutableDestination<HttpResourceModel> destination : routableDestinations) { HttpResourceModel resourceModel = destination.getDestination(); int groupMatch = destination.getGroupNameValues().size(); for (HttpMethod httpMethod : resourceModel.getHttpMethod()) { if (targetHttpMethod.equals(httpMethod)) { int exactMatch = getExactPrefixMatchCount( requestUriParts, Splitter.on('/').omitEmptyStrings().split(resourceModel.getPath())); // When there are multiple matches present, the following precedence order is used - // 1. template path that has highest exact prefix match with the url is chosen. // 2. template path has the maximum groups is chosen. // 3. finally, template path that has the longest length is chosen. if (exactMatch > maxExactMatch) { maxExactMatch = exactMatch; maxGroupMatch = groupMatch; maxPatternLength = resourceModel.getPath().length(); matchedDestinations.clear(); matchedDestinations.add(destination); } else if (exactMatch == maxExactMatch && groupMatch >= maxGroupMatch) { if (groupMatch > maxGroupMatch || resourceModel.getPath().length() > maxPatternLength) { maxGroupMatch = groupMatch; maxPatternLength = resourceModel.getPath().length(); matchedDestinations.clear(); } matchedDestinations.add(destination); } } } } return matchedDestinations; } /** @return the number of path components that match from left to right. */ private int getExactPrefixMatchCount(Iterable<String> first, Iterable<String> second) { int count = 0; for (Iterator<String> fit = first.iterator(), sit = second.iterator(); fit.hasNext() && sit.hasNext(); ) { if (fit.next().equals(sit.next())) { ++count; } else { break; } } return count; } }
/** * Call the appropriate handler for handling the httprequest. 404 if path is not found. 405 if * path is found but httpMethod does not match what's configured. * * @param request instance of {@code HttpRequest} * @param responder instance of {@code HttpResponder} to handle the request. * @return HttpMethodInfo object, null if urlRewriter rewrite returns false, also when method * cannot be invoked. * @throws HandlerException If URL rewriting fails */ public HttpMethodInfoBuilder getDestinationMethod(HttpRequest request, HttpResponder responder) throws HandlerException { if (urlRewriter != null) { try { request.setUri(URI.create(request.getUri()).normalize().toString()); if (!urlRewriter.rewrite(request, responder)) { return null; } } catch (Throwable t) { log.error("Exception thrown during rewriting of uri {}", request.getUri(), t); throw new HandlerException( HttpResponseStatus.INTERNAL_SERVER_ERROR, String.format("Caught exception processing request. Reason: %s", t.getMessage())); } } String acceptHeaderStr = request.headers().get(HttpHeaders.Names.ACCEPT); List<String> acceptHeader = (acceptHeaderStr != null) ? Arrays.asList(acceptHeaderStr.split("\\s*,\\s*")) .stream() .map(mediaType -> mediaType.split("\\s*;\\s*")[0]) .collect(Collectors.toList()) : null; String contentTypeHeaderStr = request.headers().get(HttpHeaders.Names.CONTENT_TYPE); // Trim specified charset since UTF-8 is assumed String contentTypeHeader = (contentTypeHeaderStr != null) ? contentTypeHeaderStr.split("\\s*;\\s*")[0] : null; try { String path = URI.create(request.getUri()).normalize().getPath(); List<PatternPathRouterWithGroups.RoutableDestination<HttpResourceModel>> routableDestinations = patternRouter.getDestinations(path); List<PatternPathRouterWithGroups.RoutableDestination<HttpResourceModel>> matchedDestinations = getMatchedDestination(routableDestinations, request.getMethod(), path); if (!matchedDestinations.isEmpty()) { PatternPathRouterWithGroups.RoutableDestination<HttpResourceModel> matchedDestination = matchedDestinations .stream() .filter( matchedDestination1 -> { return matchedDestination1 .getDestination() .matchConsumeMediaType(contentTypeHeader) && matchedDestination1 .getDestination() .matchProduceMediaType(acceptHeader); }) .findFirst() .get(); HttpResourceModel httpResourceModel = matchedDestination.getDestination(); // Call preCall method of handler interceptors. boolean terminated = false; ServiceMethodInfo serviceMethodInfo = new ServiceMethodInfo( httpResourceModel.getMethod().getDeclaringClass().getName(), httpResourceModel.getMethod()); for (Interceptor interceptor : interceptors) { if (!interceptor.preCall(request, responder, serviceMethodInfo)) { // Terminate further request processing if preCall returns false. terminated = true; break; } } // Call httpresource handle method, return the HttpMethodInfo Object. if (!terminated) { // Wrap responder to make post hook calls. responder = new WrappedHttpResponder(responder, interceptors, request, serviceMethodInfo); return HttpMethodInfoBuilder.getInstance() .httpResourceModel(httpResourceModel) .httpRequest(request) .httpResponder(responder) .requestInfo( matchedDestination.getGroupNameValues(), contentTypeHeader, acceptHeader); } } else if (!routableDestinations.isEmpty()) { // Found a matching resource but could not find the right HttpMethod so return 405 throw new HandlerException(HttpResponseStatus.METHOD_NOT_ALLOWED, request.getUri()); } else { throw new HandlerException( HttpResponseStatus.NOT_FOUND, String.format("Problem accessing: %s. Reason: Not Found", request.getUri())); } } catch (NoSuchElementException ex) { throw new HandlerException( HttpResponseStatus.UNSUPPORTED_MEDIA_TYPE, String.format("Problem accessing: %s. Reason: Unsupported Media Type", request.getUri()), ex); } return null; }