Java tutorial
/* * Copyright 2013 Boyle Software, Inc. * * 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.boylesoftware.web; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import javax.annotation.Resource; import javax.mail.Session; import javax.naming.InitialContext; import javax.naming.NameNotFoundException; import javax.naming.NamingException; import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.UnavailableException; import javax.validation.MessageInterpolator; import javax.validation.Validation; import javax.validation.ValidatorFactory; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.boylesoftware.web.api.Routes; import com.boylesoftware.web.impl.RequestUserLocaleFinder; import com.boylesoftware.web.impl.StandardControllerMethodArgHandlerProvider; import com.boylesoftware.web.impl.auth.NopUserRecordsCache; import com.boylesoftware.web.impl.auth.SessionlessAuthenticationService; import com.boylesoftware.web.impl.routes.RoutesRouterConfiguration; import com.boylesoftware.web.impl.view.DispatchViewSender; import com.boylesoftware.web.impl.view.MultiplexViewSender; import com.boylesoftware.web.spi.AuthenticationService; import com.boylesoftware.web.spi.ControllerMethodArgHandlerProvider; import com.boylesoftware.web.spi.RouterConfiguration; import com.boylesoftware.web.spi.UserLocaleFinder; import com.boylesoftware.web.spi.UserRecordHandler; import com.boylesoftware.web.spi.UserRecordsCache; import com.boylesoftware.web.spi.ViewSender; /** * Base class for the custom application object. Custom web-application must * provide a concrete subclass of this base class and register it as a servlet * context listener. * * @author Lev Himmelfarb */ public abstract class AbstractWebApplication implements ApplicationConfiguration, ServletContextListener { /** * Default number of threads. */ public static final int DEFAULT_ASYNC_THREADS = 10; /** * Default persistence unit name. */ public static final String DEFAULT_PU_NAME = "pu"; /** * Name of servlet context attribute used to store the web application * object. */ private static final String WEBAPP_ATTNAME = (AbstractWebApplication.class).getName() + ".WEBAPP"; /** * Servlet context. */ protected ServletContext servletContext; /** * The HTTP port. */ @Resource(name = "httpPort") private int httpPort; /** * The HTTPS port. */ @Resource(name = "httpsPort") private int httpsPort; /** * Application configuration properties. */ protected final Map<String, Object> configProperties = new ConcurrentHashMap<>(); /** * Application services. */ protected final ApplicationServices services = new ApplicationServices(this); /** * The router configuration. */ private RouterConfiguration routerConfiguration; /** * Executor service used to asynchronously process requests. */ private ExecutorService executors; /* (non-Javadoc) * @see javax.servlet.ServletContextListener#contextInitialized(javax.servlet.ServletContextEvent) */ @Override public void contextInitialized(final ServletContextEvent sce) { this.servletContext = sce.getServletContext(); sce.getServletContext().setAttribute(WEBAPP_ATTNAME, this); } /* (non-Javadoc) * @see javax.servlet.ServletContextListener#contextDestroyed(javax.servlet.ServletContextEvent) */ @Override public void contextDestroyed(final ServletContextEvent sce) { sce.getServletContext().removeAttribute(WEBAPP_ATTNAME); this.servletContext = null; } /** * Initialize the web-application. Called by the framework just before the * application is made available in the container. * * @throws UnavailableException If application cannot be initialized. * Throwing the exception makes the application fail to start. */ void onRouterInit() throws UnavailableException { // get the log final Log log = LogFactory.getLog(AbstractWebApplication.class); log.debug("initializing the web-application"); // initialize the application boolean success = false; try { // create configuration log.debug("creating configuration"); this.configure(this.configProperties); // get the authenticator final ServletContext sc = this.servletContext; log.debug("creating authenticator"); this.services.setAuthenticationService(this.getAuthenticationService(sc, this)); // get validator factory log.debug("creating validator factory"); this.services.setValidatorFactory(this.getValidatorFactory(sc, this)); // get user locale finder log.debug("creating user locale finder"); this.services.setUserLocaleFinder(this.getUserLocaleFinder(sc, this)); // create entity manager factory log.debug("creating persistence manager factory"); this.services.setEntityManagerFactory(this.getEntityManagerFactory(sc, this)); // get JavaMail session from the JNDI log.debug("attempting to find JavaMail session in the JNDI"); try { final InitialContext jndi = new InitialContext(); try { this.services .setMailSession((Session) jndi.lookup(this.getConfigProperty(MAIL_SESSION_JNDI_NAME, String.class, ApplicationServices.DEFAULT_MAIL_SESSION_JNDI_NAME))); } catch (final NameNotFoundException e) { log.debug("no JavaMail session in the JNDI, JavaMail will" + " be unavailable", e); } } catch (final NamingException e) { log.error("Error accessing JNDI.", e); } // get the router configuration log.debug("creating routes configuration"); this.routerConfiguration = this.getRouterConfiguration(sc, this, this.services); // initialize custom application log.debug("initializing custom application"); this.init(); // get the executor service log.debug("creating request processing executor service"); this.executors = this.getExecutorService(sc, this); // done log.debug("initialized successfully"); success = true; } finally { if (!success) { log.debug("initialization error"); this.cleanup(); } } } /** * Destroy the web-application. Called by the framework right after the * application is made unavailable in the container. */ void onRouterDestroy() { this.cleanup(); } /** * Clean-up the application object. */ private void cleanup() { // get the log final Log log = LogFactory.getLog(AbstractWebApplication.class); log.debug("destroying the web-application"); // shutdown the executors if (this.executors != null) { log.debug("shutting down the request processing executors..."); this.executors.shutdown(); try { boolean success = true; if (!this.executors.awaitTermination(30, TimeUnit.SECONDS)) { log.warn("could not shutdown the request processing" + " executors in 30 seconds, trying to force" + " shutdown..."); this.executors.shutdownNow(); if (!this.executors.awaitTermination(30, TimeUnit.SECONDS)) { log.error("could not shutdown the request processing" + " executors"); success = false; } } if (success) log.debug("request processing executors shut down"); } catch (final InterruptedException e) { log.warn("waiting for the request processing executors to" + " shutdown was interrupted"); this.executors.shutdownNow(); Thread.currentThread().interrupt(); } finally { this.executors = null; } } // destroy custom application log.debug("destroying custom application"); try { this.destroy(); } catch (final Exception e) { log.error("error destroying custom application", e); } // forget the router configuration this.routerConfiguration = null; // close and forget the entity manager factory final EntityManagerFactory emf = this.services.getEntityManagerFactory(); if (emf != null) { this.services.setEntityManagerFactory(null); try { log.debug("closing persistence manager factory"); emf.close(); } catch (final Exception e) { log.error("error shutting down the application", e); } } // forget user locale finder this.services.setUserLocaleFinder(null); // close and forget the validator factory final ValidatorFactory vf = this.services.getValidatorFactory(); if (vf != null) { this.services.setValidatorFactory(null); try { log.debug("closing validator factory"); vf.close(); } catch (final Exception e) { log.error("error shutting down the application", e); } } // forget the authentication service this.services.setAuthenticationService(null); } /** * Create application configuration properties. The method is called during * the application initialization before the {@link #init()}. The * configuration properties are made available during the application * runtime via the {@link #getConfigProperty(String, Class, Object)} method. * * <p>Can be overridden by the subclass. The default implementation does * nothing. * * @param config The configuration properties. The implementation can add * configuration properties to this map. * * @throws UnavailableException If an error happens. Throwing this exception * will cause the application to fail to start. */ @SuppressWarnings("unused") protected void configure(final Map<String, Object> config) throws UnavailableException { // nothing } /** * Initialize custom application. * * <p>Can be overridden by the subclass. Default implementation does * nothing. * * @throws UnavailableException If an error happens. Throwing this exception * will cause the application to fail to start. */ @SuppressWarnings("unused") protected void init() throws UnavailableException { // nothing } /** * Destroy custom application. Can be overridden by the subclass. Default * implementation does nothing. */ protected void destroy() { // nothing } /** * Get web-application object for the specified servlet context. The method * can be used in custom application components that run outside controllers * (controllers can get the application object as a controller method * argument), such as JSP views. * * @param sc The servlet context. * * @return The web-application object. * * @throws UnavailableException If web-application object is not available * for the specified servlet context. */ public static AbstractWebApplication getApplication(final ServletContext sc) throws UnavailableException { final AbstractWebApplication webapp = (AbstractWebApplication) sc.getAttribute(WEBAPP_ATTNAME); if (webapp == null) throw new UnavailableException("WebApplication listener has not been configured."); return webapp; } /** * Get application's HTTPS port. * * @return The HTTPS port. */ public int getHTTPSPort() { return this.httpsPort; } /** * Get application's plan HTTP port. * * @return The HTTP port. */ public int getHTTPPort() { return this.httpPort; } /* (non-Javadoc) * @see com.boylesoftware.web.ApplicationConfiguration#getConfigProperty(java.lang.String, java.lang.Class, java.lang.Object) */ @SuppressWarnings("unchecked") @Override public <T> T getConfigProperty(final String propName, final Class<T> propClass, final T defaultValue) { final Object valObj = this.configProperties.get(propName); if (valObj == null) return defaultValue; if (propClass.isAssignableFrom(valObj.getClass())) return (T) valObj; if (valObj instanceof String) { if ((String.class).isAssignableFrom(propClass)) return (T) valObj; final String valStr = (String) valObj; if ((Integer.class).isAssignableFrom(propClass)) { final T val = (T) Integer.valueOf(valStr); this.configProperties.put(propName, val); return val; } if ((Long.class).isAssignableFrom(propClass)) { final T val = (T) Long.valueOf(valStr); this.configProperties.put(propName, val); return val; } if ((Boolean.class).isAssignableFrom(propClass)) { final T val = (T) Boolean.valueOf(valStr); this.configProperties.put(propName, val); return val; } throw new IllegalArgumentException( "Unsupported application configuration property type " + propClass.getName() + "."); } throw new RuntimeException("Application configuration property " + propName + " has unexpected type " + valObj.getClass().getName() + "."); } /** * Get router configuration API. * * @return The router configuration API. */ public Routes getRoutes() { return this.routerConfiguration.getRoutes(); } /** * Get entity manager factory. * * @return Entity manager factory. */ EntityManagerFactory getEntityManagerFactory() { return this.services.getEntityManagerFactory(); } /** * Get authentication service. * * @return Authentication service. */ AuthenticationService<?> getAuthenticationService() { return this.services.getAuthenticationService(); } /** * Get router configuration. * * @return The router configuration. */ RouterConfiguration getRouterConfiguration() { return this.routerConfiguration; } /** * Get executor service for asynchronous request processing. * * @return The executor service. */ ExecutorService getExecutorService() { return this.executors; } /** * Get executor service. This method is called once during the application * initialization. The executor service is automatically shut down by the * framework when the application goes down. * * <p>Default implementation returns a fixed size thread pool with number * of threads specified by the * {@link ApplicationConfiguration#ASYNC_THREADS} application configuration * property. If the application configuration property is undefined, default * number of threads is {@value #DEFAULT_ASYNC_THREADS}. * * @param sc Servlet context. * @param config Application configuration. * * @return The executor service. * * @throws UnavailableException If the executor service is unavailable. * Throwing this exception makes the web-application fail to start. */ @SuppressWarnings("unused") protected ExecutorService getExecutorService(final ServletContext sc, final ApplicationConfiguration config) throws UnavailableException { final ThreadGroup threadGroup = new ThreadGroup("AsyncExecutors"); final int numThreads = config.getConfigProperty(ApplicationConfiguration.ASYNC_THREADS, Integer.class, Integer.valueOf(DEFAULT_ASYNC_THREADS)).intValue(); return Executors.newFixedThreadPool(numThreads, new ThreadFactory() { private int nextThreadNum = 0; @Override public Thread newThread(final Runnable r) { final String threadName = "async-executor-" + (this.nextThreadNum++); LogFactory.getLog(this.getClass()) .debug("starting asynchronous request processing thread " + threadName); return new Thread(threadGroup, r, threadName); } }); } /** * Get the authentication service. This method is called once during the * application initialization. * * <p>Default implementation returns a * {@link SessionlessAuthenticationService}. * * @param sc Servlet context. * @param config Application configuration. * * @return The authenticator. * * @throws UnavailableException If authentication service is unavailable. * Throwing this exception makes the web-application fail to start. */ @SuppressWarnings({ "unchecked", "rawtypes" }) protected AuthenticationService<?> getAuthenticationService(final ServletContext sc, final ApplicationConfiguration config) throws UnavailableException { return new SessionlessAuthenticationService(this.getUserRecordHandler(sc, config), this.getUserRecordsCache(sc, config)); } /** * Get user record handler used by the authentication service. This method * is called once during the application initialization. * * <p>Since there is no generic user record handler implementation, this * method, unless overridden, throws an {@link UnavailableException}, so * that if the application uses an authentication service that works with * persistent user account records, it must override this method and provide * an application-specific implementation of the user record handler. * * @param sc Servlet context. * @param config Application configuration. * * @return User record handler. * * @throws UnavailableException If user record handler is unavailable. * Throwing this exception makes the web-application fail to start. */ @SuppressWarnings("unused") protected UserRecordHandler<?> getUserRecordHandler(final ServletContext sc, final ApplicationConfiguration config) throws UnavailableException { throw new UnavailableException("Application uses user record" + " authentication service implementation, but user record" + " handler is not provided."); } /** * Get user records cache implementation used by the authentication service. * This method is called once during the application initialization. * * <p>Default implementation returns a {@link NopUserRecordsCache}. * * @param sc Servlet context. * @param config Application configuration. * * @return User records cache implementation. * * @throws UnavailableException If user records cache implementation is * unavailable. Throwing this exception makes the web-application fail to * start. */ @SuppressWarnings("unused") protected UserRecordsCache<?> getUserRecordsCache(final ServletContext sc, final ApplicationConfiguration config) throws UnavailableException { return new NopUserRecordsCache<>(); } /** * Get entity manager factory. This method is called once during the * application initialization. The entity manager factory is automatically * closed by the framework when the application shuts down. * * <p>Default implementation uses * {@link Persistence#createEntityManagerFactory(String)} method to create * the entity manager factory. The persistence unit name is taken from the * {@link ApplicationConfiguration#PU_NAME} application configuration * property with default name {@value #DEFAULT_PU_NAME}. * * @param sc Servlet context. * @param config Application configuration. * * @return Entity manager factory. * * @throws UnavailableException If entity manager factory is unavailable. * Throwing this exception makes the web-application fail to start. */ @SuppressWarnings("unused") protected EntityManagerFactory getEntityManagerFactory(final ServletContext sc, final ApplicationConfiguration config) throws UnavailableException { return Persistence.createEntityManagerFactory( config.getConfigProperty(ApplicationConfiguration.PU_NAME, String.class, DEFAULT_PU_NAME)); } /** * Get validator factory used for validating user input (such as submitted * HTML forms). This method is called once during the application * initialization. * * <p>Default implementation uses default validation provider (see * {@link Validation#byDefaultProvider()} and optionally a custom * {@link MessageInterpolator} returned by the * {@link #getValidatorMessageInterpolator} method. * * @param sc Servlet context. * @param config Application configuration. * * @return Validator factory. * * @throws UnavailableException If validator factory is unavailable. * Throwing this exception makes the web-application fail to start. */ protected ValidatorFactory getValidatorFactory(final ServletContext sc, final ApplicationConfiguration config) throws UnavailableException { final MessageInterpolator messageInterpolator = this.getValidatorMessageInterpolator(sc, config); if (messageInterpolator == null) return Validation.buildDefaultValidatorFactory(); return Validation.byDefaultProvider().configure().messageInterpolator(messageInterpolator) .buildValidatorFactory(); } /** * Get message interpolator for the user input validator. The method is * called once during the application initialization. * * <p>Default implementation returns {@code null} to use the default * message interpolator. * * @param sc Servlet context. * @param config Application configuration. * * @return Message interpolator, or {@code null} to use the default. * * @throws UnavailableException If an error happens getting the message * interpolator. Throwing this exception makes the web-application fail to * start. */ @SuppressWarnings("unused") protected MessageInterpolator getValidatorMessageInterpolator(final ServletContext sc, final ApplicationConfiguration config) throws UnavailableException { return null; } /** * Get user locale finder. This method is called during the application * initialization. * * <p>Default implementation returns {@link RequestUserLocaleFinder}. * * @param sc Servlet context. * @param config Application configuration. * * @return User locale finder. * * @throws UnavailableException If user locale finder is unavailable. * Throwing this exception makes the web-application fail to start. */ @SuppressWarnings("unused") protected UserLocaleFinder<?> getUserLocaleFinder(final ServletContext sc, final ApplicationConfiguration config) throws UnavailableException { return new RequestUserLocaleFinder(); } /** * Get the router configuration. This method is called once during the * application initialization. * * <p>Default implementation returns {@link RoutesRouterConfiguration} * configured with view sender and controller method argument handler * provider returned by the {@link #getViewSender} and * {@link #getControllerMethodArgHandlerProvider} methods. * * @param sc Servlet context. * @param config Application configuration. * @param appServices Application services. * * @return The routes. * * @throws UnavailableException If routing configuration is unavailable. * Throwing this exception makes the web-application fail to start. */ protected RouterConfiguration getRouterConfiguration(final ServletContext sc, final ApplicationConfiguration config, final ApplicationServices appServices) throws UnavailableException { return new RoutesRouterConfiguration(sc, appServices, this.getControllerMethodArgHandlerProvider(sc, config, appServices), this.getViewSender(sc, config, appServices)); } /** * Get view sender. This method is called once during the application * initialization. * * <p>Default implementation returns a {@link MultiplexViewSender} * configured with all view senders provided by the framework out-of-the-box * mapped using corresponding template file extensions. * * @param sc Servlet context. * @param config Application configuration. * @param appServices Application services. * * @return View sender provider. * * @throws UnavailableException If view sender provider is unavailable. * Throwing this exception makes the web-application fail to start. */ @SuppressWarnings("unused") protected ViewSender getViewSender(final ServletContext sc, final ApplicationConfiguration config, final ApplicationServices appServices) throws UnavailableException { final MultiplexViewSender sender = new MultiplexViewSender(); sender.addPattern(".*\\.jspx?", new DispatchViewSender()); return sender; } /** * Get controller method argument handlers provider. This method is called * once during the application initialization. * * <p>Default implementation returns a * {@link StandardControllerMethodArgHandlerProvider}. * * @param sc Servlet context. * @param config Application configuration. * @param appServices Application services. * * @return Controller method argument handlers provider. * * @throws UnavailableException If controller method argument handlers * provider is unavailable. Throwing this exception makes the * web-application fail to start. */ @SuppressWarnings("unused") protected ControllerMethodArgHandlerProvider getControllerMethodArgHandlerProvider(final ServletContext sc, final ApplicationConfiguration config, final ApplicationServices appServices) throws UnavailableException { return new StandardControllerMethodArgHandlerProvider(appServices); } }