/** * Copyright (c) 2013-2014 Oculus Info Inc. * http://www.oculusinfo.com/ * <p/> * Released under the MIT License. * <p/> * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies * of the Software, and to permit persons to whom the Software is furnished to do * so, subject to the following conditions: * <p/> * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * <p/> * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.oculusinfo.tile.servlet; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.util.Collection; import java.util.Map; import java.util.Map.Entry; import javax.servlet.FilterChain; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.oculusinfo.tile.util.ResourceHelper; import net.sf.ehcache.CacheManager; import net.sf.ehcache.constructs.web.AlreadyCommittedException; import net.sf.ehcache.constructs.web.AlreadyGzippedException; import net.sf.ehcache.constructs.web.GenericResponseWrapper; import net.sf.ehcache.constructs.web.Header; import net.sf.ehcache.constructs.web.PageInfo; import net.sf.ehcache.constructs.web.ShutdownListener; import net.sf.ehcache.constructs.web.filter.SimplePageCachingFilter; import org.restlet.Application; import org.restlet.Context; import org.restlet.engine.header.HeaderConstants; import org.restlet.resource.ServerResource; import org.restlet.routing.Router; import org.restlet.routing.TemplateRoute; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.io.Files; import com.google.inject.Provides; import com.google.inject.servlet.ServletModule; /** * This module sets up all REST related infrastructure, including: * <ul> * <li>Binding the /rest/* endpoint to the RestServlet</li> * <li>Binding a Restlet Application object that is configured with all ServerResources * mapped to paths in other modules</li> * </ul> * * @author rharper */ public class RestModule extends ServletModule implements ServletContextListener { private static final String REST_BASE = "/rest"; /** * This is an unfortunate business but seems to the only way to prevent ehcache from * caching pages that have already explicitly been marked with a no cache header. If * it shouldn't be cached on the client it shouldn't be cached here. * * @author djonker */ private static class CustomPageInfo extends PageInfo { private static final long serialVersionUID = 1L; private boolean cacheable; /** * @param statusCode * @param contentType * @param cookies * @param body * @param storeGzipped * @param timeToLiveSeconds * @param headers * @param cacheable * @throws AlreadyGzippedException */ public CustomPageInfo( int statusCode, String contentType, Collection cookies, byte[] body, boolean storeGzipped, long timeToLiveSeconds, Collection<Header<? extends Serializable>> headers, boolean cacheable ) throws AlreadyGzippedException { super( statusCode, contentType, cookies, body, storeGzipped, timeToLiveSeconds, headers ); this.cacheable = cacheable; } /* (non-Javadoc) * ehCache source code tells us that this is the only post-request criteria ehCache considers when * deciding whether to cache a response, and that it's not used to indicate anything else. * * @see net.sf.ehcache.constructs.web.PageInfo#isOk() */ @Override public boolean isOk() { return cacheable && super.isOk(); } } /** * A trivial extension to the ehcache-web that ensures only GET method * requests are cached, all others are passed through. */ private static class CustomPageCachingFilter extends SimplePageCachingFilter { @Override protected void doFilter( HttpServletRequest request, HttpServletResponse response, FilterChain chain ) throws Exception { if ( "GET".equals( request.getMethod() ) ) { // Go through cache if ( response.isCommitted() ) { throw new AlreadyCommittedException( "Response already committed before doing buildPage." ); } logRequestHeaders( request ); PageInfo pageInfo = buildPageInfo( request, response, chain ); // Always write the response. This is necessary if // we want the ajax error handler to be able to // display exception messages from the server. writeResponse( request, response, pageInfo ); } else { // Don't go through cache chain.doFilter( request, response ); } } /* (non-Javadoc) * @see net.sf.ehcache.constructs.web.filter.CachingFilter#buildPage(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain) */ @Override protected PageInfo buildPage( HttpServletRequest request, HttpServletResponse response, FilterChain chain ) throws Exception { // Invoke the next entity in the chain final ByteArrayOutputStream outstr = new ByteArrayOutputStream(); final GenericResponseWrapper wrapper = new GenericResponseWrapper( response, outstr ); chain.doFilter( request, wrapper ); wrapper.flush(); final long timeToLiveSeconds = blockingCache.getCacheConfiguration() .getTimeToLiveSeconds(); boolean cacheable = true; for ( Header<? extends Serializable> header : wrapper.getAllHeaders() ) { if ( HeaderConstants.HEADER_CACHE_CONTROL.equals( header.getName() ) && HeaderConstants.CACHE_NO_CACHE.equals( header.getValue() ) ) { cacheable = false; } } // Return the page info return new CustomPageInfo( wrapper.getStatus(), wrapper.getContentType(), wrapper.getCookies(), outstr.toByteArray(), true, timeToLiveSeconds, wrapper.getAllHeaders(), cacheable ); } } final Logger logger = LoggerFactory.getLogger( getClass() ); private final ServletContext context; private CustomPageCachingFilter filter; public RestModule( ServletContext context ) { this.context = context; } @Override protected void configureServlets() { /* * Servlets */ // Handle all RPC requests with a servlet bound to the RPCServlet Name logger.debug( "Setting REST base path to '" + REST_BASE + "/*'" ); serve( REST_BASE + "/*" ).with( RestletServlet.class ); // Read the ehCache property from web.xml or override-web.xml String filename = context.getInitParameter( "ehcacheConfig" ); try { logger.info( "Loading ehcache configuration..." ); if ( System.getProperty( "ehcache.disk.store.dir" ) == null ) { System.setProperty( "ehcache.disk.store.dir", Files.createTempDir().getPath() ); } // Load properties InputStream inp = ResourceHelper.getStreamForPath(filename, "res:///ehcache.xml"); // Create cache manager with provided configuration CacheManager.create( inp ); try { inp.close(); } catch ( IOException ioe ) { logger.warn( "Failed to close ehcache configuration input stream.", ioe ); } } catch ( IOException e ) { // Failed to load properties, error addError( e ); } filter = new CustomPageCachingFilter(); // Filter all REST calls filter( REST_BASE + "/*" ).through( filter ); } /** * Creates the restlet application that will be used to handle all rest calls * Takes a map of paths to resource classes * <p/> * Use the following three lines to access the routing multibinder: * TypeLiteral<String> pathType = new TypeLiteral<String>() {}; * TypeLiteral<Class<? extends ServerResource>> clazzType = new TypeLiteral<Class<? extends ServerResource>>() {}; * MapBinder<String, Class<? extends ServerResource>> resourceBinder = MapBinder.newMapBinder(binder(), pathType, clazzType); * resourceBinder.bind("/my/path").toInstance(MyResource.class); */ @Provides Application createApplication( FinderFactory factory, Map<String, Class<? extends ServerResource>> routes ) { Context context = new Context(); Application application = new Application(); application.setContext( context ); Router router = new Router( context ); // Set binding rules here for ( Entry<String, Class<? extends ServerResource>> entry : routes.entrySet() ) { final Class<? extends ServerResource> resource = entry.getValue(); logger.info( "Binding '" + entry.getKey() + "' to " + resource ); router.attach( entry.getKey(), factory.finder( resource ) ); } application.setInboundRoot( router ); return application; } @Override public void contextInitialized( ServletContextEvent sce ) { } @Override public void contextDestroyed( ServletContextEvent sce ) { final ShutdownListener listener = new ShutdownListener(); listener.contextDestroyed( sce ); } }