Java tutorial
/** * Copyright (C) 2013 Steve Owens (DoItNext.com) http://www.doitnext.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.doitnext.http.router; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Required; import org.springframework.web.HttpRequestHandler; import com.doitnext.http.router.annotations.enums.HttpMethod; import com.doitnext.http.router.exceptions.Http404Exception; import com.doitnext.http.router.exceptions.Http405Exception; import com.doitnext.http.router.exceptions.Http406Exception; import com.doitnext.http.router.exceptions.Http415Exception; import com.doitnext.http.router.exceptions.Http500Exception; import com.doitnext.http.router.responsehandlers.DefaultErrorHandler; import com.doitnext.http.router.responsehandlers.ResponseHandler; import com.doitnext.pathutils.Path; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.google.common.collect.ImmutableSortedSet; public class RestRouterServlet implements HttpRequestHandler, InitializingBean, EndpointDumper, DynamicRouteConsumer { /** * Serial version id for this servlet */ @SuppressWarnings("unused") private static final long serialVersionUID = 149227713315378579L; private static Logger logger = LoggerFactory.getLogger(RestRouterServlet.class); private volatile ImmutableSortedSet<Route> routes; // Spring Injected private String restPackageRoot; private String pathPrefix = ""; private DynamicEndpointResolver dynamicEndpointResolver = null; private MethodInvoker methodInvoker = new DefaultInvoker(); private EndpointResolver endpointResolver = new DefaultEndpointResolver(); private ResponseHandler errorHandler = new DefaultErrorHandler(); private ObjectMapper objectMapper = new ObjectMapper(); public RestRouterServlet() { objectMapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING); objectMapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); } @Required public void setRestPackageRoot(String value) { this.restPackageRoot = value; } public void setPathPrefix(String pathPrefix) { this.pathPrefix = pathPrefix; } public void setMethodInvoker(MethodInvoker methodInvoker) { this.methodInvoker = methodInvoker; } public void setEndpointResolver(EndpointResolver endpointResolver) { this.endpointResolver = endpointResolver; } public void setDynamicEndpointResolver(DynamicEndpointResolver value) { this.dynamicEndpointResolver = value; } public void setErrorHandler(ResponseHandler errorHandler) { this.errorHandler = errorHandler; } public SortedSet<Route> getRoutes() { return routes; } @Override public void afterPropertiesSet() throws Exception { routes = endpointResolver.resolveEndpoints(pathPrefix, restPackageRoot); if (routes.isEmpty()) { logger.warn( "On init no routes established. This servlet will not respond to any requests while in this state."); } else { logger.debug(String.format("Identified %d routes.", routes.size())); } } /* (non-Javadoc) * @see com.doitnext.http.router.DynamicRouteConsumer#fireUpdateRoutes() */ public void fireUpdateRoutes() { if (dynamicEndpointResolver != null) { dynamicEndpointResolver.updateRoutes(this, routes); } else throw new IllegalStateException("No dynamicEndpointResolver set."); } /* (non-Javadoc) * @see com.doitnext.http.router.DynamicRouteConsumer#routesUpdated(com.google.common.collect.ImmutableSortedSet) */ public void routesUpdated(ImmutableSortedSet<Route> newRoutes) { this.routes = newRoutes; } protected boolean do404(HttpMethod method, HttpServletRequest req, HttpServletResponse resp) { if (logger.isTraceEnabled()) { logger.trace(String.format("%s %s returns 404", method.name(), req.getRequestURI())); } resp.setStatus(404); return errorHandler.handleResponse(null, req, resp, new Http404Exception(req.getRequestURI())); } protected boolean do406(HttpMethod method, HttpServletRequest req, HttpServletResponse resp) { resp.setStatus(406); return errorHandler.handleResponse(null, req, resp, new Http406Exception()); } protected boolean do415(HttpMethod method, HttpServletRequest req, HttpServletResponse resp) { resp.setStatus(415); String contentType = req.getHeader("Content-Type"); return errorHandler.handleResponse(null, req, resp, new Http415Exception(contentType)); } protected boolean do405(HttpMethod method, List<String> allowedMethods, HttpServletRequest req, HttpServletResponse resp) { resp.setStatus(405); return errorHandler.handleResponse(null, req, resp, new Http405Exception(method, allowedMethods)); } protected boolean do500(HttpMethod method, HttpServletRequest req, HttpServletResponse resp, Throwable error) { resp.setStatus(500); return errorHandler.handleResponse(null, req, resp, new Http500Exception(error)); } protected boolean routeRequest(HttpMethod method, HttpServletRequest req, HttpServletResponse resp) { SortedSet<Route> routes = this.routes; List<PathMatch> pathMatches = new ArrayList<PathMatch>(); String pathString = req.getPathInfo(); for (Route route : routes) { if (logger.isTraceEnabled()) { logger.trace(String.format("Trying to match path '%s' against '%s'", pathString, route.toString())); } Path path = route.getPathTemplate().match(pathString); if (path != null) { if (logger.isTraceEnabled()) { logger.trace(String.format("Matched path '%s' against '%s'", pathString, route)); } pathMatches.add(new PathMatch(route, path)); } } if (pathMatches.isEmpty()) return do404(method, req, resp); // Resource not found else if (logger.isTraceEnabled()) logger.trace(String.format("There are %d routes that match by uri path.", pathMatches.size())); List<PathMatch> pathMatchesByPathAndMethod = new ArrayList<PathMatch>(); Set<HttpMethod> allowedMethods = new TreeSet<HttpMethod>(); for (PathMatch pm : pathMatches) { if (pm.getRoute().getHttpMethod().equals(method)) { pathMatchesByPathAndMethod.add(pm); if (logger.isTraceEnabled()) { logger.trace(String.format("Http request method: %s matches route %s", method.name(), pm.getRoute())); } } else { allowedMethods.add(pm.getRoute().getHttpMethod()); if (logger.isTraceEnabled()) { logger.trace(String.format( "Http request method: %s does not match route %s. This route will be excluded from further consideration.", method.name(), pm.getRoute())); } } } if (pathMatchesByPathAndMethod.isEmpty()) { List<String> am = new ArrayList<String>(); for (HttpMethod m : allowedMethods) am.add(m.name()); return do405(method, am, req, resp); } String acceptTypes = req.getHeader("Accept"); String accepts[] = acceptTypes.split(","); List<AcceptKey> acceptKeys = new ArrayList<AcceptKey>(); for (String accept : accepts) { acceptKeys.add(new AcceptKey(accept.trim())); } List<PathMatch> pathMatchesByResponseType = new ArrayList<PathMatch>(); for (PathMatch pm : pathMatchesByPathAndMethod) { // If this is a wildcard return method just add it if (pm.getRoute().isWildcardReturn()) { pathMatchesByResponseType.add(pm); } else { for (AcceptKey acceptKey : acceptKeys) { if (acceptKey.matches(pm.getRoute())) { pathMatchesByResponseType.add(pm); if (logger.isTraceEnabled()) { logger.trace( String.format("Accept key: %s matches route %s", acceptKey, pm.getRoute())); } } else if (logger.isTraceEnabled()) { logger.trace(String.format( "Accept key: %s does not match route %s. This route will be excluded from further consideration.", acceptKey, pm.getRoute())); } } } } // If a route exists with empty return type and the request has empty return // type then remove routes that have non empty return types (except for wildcard returns) for (AcceptKey key : acceptKeys) { if (StringUtils.isEmpty(key.getReturnType())) { for (PathMatch pm : pathMatchesByResponseType) { if (pm.getRoute().getReturnFormat().equalsIgnoreCase(key.getReturnFormat()) && !StringUtils.isEmpty(pm.getRoute().getReturnType()) && !pm.getRoute().isWildcardReturn()) { pathMatchesByResponseType.remove(pm); break; } } } } if (pathMatchesByResponseType.isEmpty()) return do406(method, req, resp); else if (logger.isTraceEnabled()) logger.trace(String.format("There are %d routes that match by response type.", pathMatchesByResponseType.size())); List<PathMatch> pathMatchesByContentType = new ArrayList<PathMatch>(); String contentTypeHeader = req.getHeader("Content-Type"); ContentTypeKey contentTypeKey = new ContentTypeKey(contentTypeHeader); for (PathMatch pm : pathMatchesByResponseType) { Route route = pm.getRoute(); if (route.isWildcardConsumer()) pathMatchesByContentType.add(pm); else if (contentTypeKey.matches(route)) { pathMatchesByContentType.add(pm); if (logger.isTraceEnabled()) { logger.trace( String.format("Content type key: %s matches route %s", contentTypeKey, pm.getRoute())); } else if (logger.isTraceEnabled()) { logger.trace(String.format( "Content type key: %s does not match route %s. This route will be excluded from further consideration.", contentTypeKey, pm.getRoute())); } } } if (pathMatchesByContentType.isEmpty()) return do415(method, req, resp); else if (logger.isTraceEnabled()) { logger.trace(String.format("There are %d routes that match by request type.", pathMatchesByContentType.size())); } List<PathMatch> pathMatchesFinalCandidates = pathMatchesByContentType; if (logger.isTraceEnabled()) { logger.trace(String.format( "There are %d routes that match by all criteria selecting most specific route. Final candidates:", pathMatchesFinalCandidates.size())); for (PathMatch pm : pathMatchesFinalCandidates) logger.trace(String.format("Final candidate: ", pm.getRoute())); } PathMatch selectedMatch = pathMatchesFinalCandidates.get(0); for (PathMatch pm : pathMatchesFinalCandidates) { PathMatch prelimSelection = selectedMatch; if (pm != selectedMatch) { if (StringUtils.isEmpty(selectedMatch.getRoute().getReturnType()) && !StringUtils.isEmpty(pm.getRoute().getReturnType())) { selectedMatch = pm; } else if (StringUtils.isEmpty(selectedMatch.getRoute().getRequestType()) && !StringUtils.isEmpty(pm.getRoute().getRequestType())) { selectedMatch = pm; } } if (logger.isTraceEnabled() && prelimSelection != selectedMatch) { logger.trace(String.format("Route %s is more specific than %s", selectedMatch, prelimSelection)); } } if (logger.isTraceEnabled()) logger.trace(String.format("Route chosen for invocation %s", selectedMatch)); try { return methodInvoker.invokeMethod(method, selectedMatch, req, resp).handled; } catch (Throwable t) { return do500(method, req, resp, t); } } @Override public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpMethod httpMethod = HttpMethod.valueOf(request.getMethod().toUpperCase()); if (!routeRequest(httpMethod, request, response)) logger.error(String.format("Failed to handle %s request.", httpMethod.name())); } @Override public List<String> getReturnFormats() { return Arrays.asList("application/json"); } @Override public void dumpEndpoints(HttpServletRequest req, HttpServletResponse resp) throws Exception { List<String> endpointPaths = new ArrayList<String>(); for (Route route : this.routes) { endpointPaths.add(route.getPathTemplate().getLexicalPath()); } resp.setStatus(200); byte bytes[] = objectMapper.writeValueAsBytes(endpointPaths); resp.setContentLength(bytes.length); resp.setContentType("application/json"); resp.getOutputStream().write(bytes); resp.getOutputStream().close(); } }