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

js.servlet.TinyContainer Maven / Gradle / Ivy

Go to download

Web Tiny Container is a service oriented Servlet extension that allows for reflexive Java methods invocation via HTTP.

There is a newer version: 1.3.0
Show newest version
package js.servlet;

import java.io.File;
import java.security.Principal;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Properties;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

import js.container.Container;
import js.container.InstanceScope;
import js.converter.ConverterRegistry;
import js.core.AppContext;
import js.core.Factory;
import js.core.SecurityContext;
import js.lang.BugError;
import js.lang.Config;
import js.lang.ConfigBuilder;
import js.lang.ConfigException;
import js.log.Log;
import js.log.LogFactory;
import js.util.Strings;

/**
 * Container specialization for web applications. This class extends {@link Container} adding implementation for
 * {@link InstanceScope#SESSION}, application context services and security context. Tiny container instance is accessible to
 * application code through {@link AppContext} interface.
 * 

* This class also implements {@link SecurityContext} services. For servlet container authentication this class delegates HTTP * request related services. For application provided authentication this class uses HTTP session to handle {@link Principal} * supplied via {@link #login(Principal)}. * *

Servlet Container Integration

*

* Servlet container creates and destroy tiny container instance via event listeners. This class implements both servlet context * and HTTP session event listeners and should be declared into application deployment descriptor. * *

 * <listener>
 * 	<listener-class>js.servlet.TinyContainer</listener-class>
 * </listener>
 * 
*

* Event handlers manage tiny container life cycle. There is a single tiny container instance per web application created by * servlet container and initialized via {@link #contextInitialized(ServletContextEvent)} event handler. When web application is * unloaded tiny container is destroyed by {@link #contextDestroyed(ServletContextEvent)}. On first tiny container creation * takes care to initialize server global state. This class also track HTTP sessions creation and destroy for debugging. * *

Application Boostrap

* Here are overall steps performed by bootstrap logic. It is executed for every web application deployed on server. *
    *
  • servlet container creates tiny container instance because is declared as listener on deployment descriptor, *
  • super-container and tiny container constructors are executed, *
  • servlet container invokes {@link #contextInitialized(ServletContextEvent)} on newly created tiny container instance, *
  • from now on logic is executed by above event handler: *
  • initialize {@link #contextParameters} from external descriptors, *
  • create {@link TinyConfigBuilder} that parses application descriptor, *
  • configure tiny container with created configuration object, see {@link #config(Config)}, *
  • bind tiny container instance to master factory, see {@link Factory#bind(js.core.AppFactory)}, *
  • finalize tiny container creation by calling {@link #start()}. *
*

* If tiny container is successfully configured and started, bootstrap logic stores its reference on servlet context attribute * {@link #ATTR_INSTANCE}. Anyway, if tiny container fails to start for some reason, dump stack trace and leave attribute null. * {@link AppServlet#init(javax.servlet.ServletConfig)} and {@link RequestPreprocessor#init(javax.servlet.FilterConfig)} test * tiny container attribute and if found null mark servlet as unavailable. This way a web application that fails to start tiny * container will have request preprocessor and all servlets unavailable and is not able to process any requests. *

* Implementation note: It is assumed that {@link #contextInitialized(ServletContextEvent)} is invoked before any servlet * initialization via {@link AppServlet#init(javax.servlet.ServletConfig)}. Servlet specification does provide explicit evidence * for this prerequisite. Anyway there is something that can lead to this conclusion on section 10.12, see below; also found * support on API-DOC. Here is the relevant excerpt: All ServletContextListeners are notified of context * initialization before any filter or servlet in the web application is initialized. * *

* For completeness here are web application deployment steps, excerpt from servlet specification 3.0, section 10.12: *

    *
  • Instantiate an instance of each event listener identified by a listener element in the deployment * descriptor. *
  • For instantiated listener instances that implement ServletContextListener, call the contextInitialized() method. *
  • Instantiate an instance of each filter identified by a filter element in the deployment descriptor and call * each filter instance’s init() method. *
  • Instantiate an instance of each servlet identified by a servlet element that includes a * load-on-startup element in the order defined by the load-onstartup element values, and call each servlet * instance’s init() method. *
* * @author Iulian Rotaru * @version final */ public class TinyContainer extends Container implements ServletContextListener, HttpSessionListener, AppContext { /** Server global state and applications logger initialization. */ private static final Server server = new Server(); /** Class logger. */ private static final Log log = LogFactory.getLog(TinyContainer.class); /** Container instance is stored on servlet context with this attribute name. */ public static final String ATTR_INSTANCE = "js.servlet.WebContainer.instance"; /** Session attribute name for principal storage when authentication is provided by application. */ public static final String ATTR_PRINCIPAL = "js.servlet.WebContainer.principal"; /** Context name for testing. */ private static final String TEST_CONTEXT_NAME = "test-app"; /** * Server and container properties loaded from context parameters defined on external descriptors. Context parameters are * optional and this properties instance can be empty. If present, context parameters are used by {@link TinyConfigBuilder} * to inject variables. Also can be retrieved by application using {@link #getProperty(String)}. *

* Tiny container uses {@link ServletContext#getInitParameter(String)} to load this context parameters. Context parameters * source may depend on web server implementation but context-param from deployment descriptor is always * supported. */ private final Properties contextParameters = new Properties(); /** The name of web application that own this tiny container. Default value to {@link #TEST_CONTEXT_NAME}. */ private String appName = TEST_CONTEXT_NAME; /** * Application private storage. Privateness is merely a good practice rather than enforced by some system level rights * protection. So called private directory is on working directory and has context name. Files returned by * {@link #getAppFile(String)} are always relative to this private directory. */ private File privateDir; /** * Optional login realm, default to web application context name. Basic authentication realm sent by servlets when client * attempt to access non authorized resource. *

* Basic authentication realm is loaded from application descriptor, login section. If not configured uses the * context name. * *

	 * <login>
	 * 	<property name="realm" value="Fax2e-mail" />
	 * 	...
	 * </login>
	 * 
*/ private String loginRealm; /** * Location for application login page, null if not configured. This field value is loaded from application descriptor. * Location can be relative or absolute to servlet container root, in which case starts with path separator. Container takes * care to convert to absolute location if configured value is relative. * *

* Login page location is loaded from application descriptor, login section. * *

	 * <login>
	 * 	...
	 * 	<property name="page" value="index.htm" />
	 * </login>
	 * 
* *

* This login page location is an alternative to servlet container declarative form-login-page from * login-config section from deployment descriptor. Anyway, if application has private resources accessed via * XHR, this login page location is mandatory. Otherwise client agent default login form is used when XHR attempt to access * not authorized resources. */ private String loginPage; /** Create tiny container instance. */ public TinyContainer() { super(); log.trace("TinyContainer()"); registerScopeFactory(new SessionScopeFactory(this)); } @Override public void config(Config config) throws ConfigException { super.config(config); // by convention configuration object name is the web application name appName = config.getName(); privateDir = server.getAppDir(appName); if (!privateDir.exists()) { privateDir.mkdir(); } Config loginConfig = config.getChild("login"); if (loginConfig != null) { loginRealm = loginConfig.getProperty("realm", appName); loginPage = loginConfig.getProperty("page"); if (loginPage != null && !loginPage.startsWith("/")) { loginPage = Strings.concat('/', appName, '/', loginPage); } } } // -------------------------------------------------------------------------------------------- // SERVLET CONTAINER LISTENERS /** * Implements tiny container boostrap logic. See class description for overall performed steps. Also loads context * parameters from external descriptors using {@link ServletContext#getInitParameter(String)}. *

* Context initialized listener is not allowed to throw exceptions and it seems there is no standard way to ask servlet * container to abort launching the web application. Tiny container solution is to leave servlet context attribute * {@link #ATTR_INSTANCE} null. On {@link AppServlet#init(javax.servlet.ServletConfig)} mentioned attribute is tested for * null and, if so, permanently mark servlet unavailable. *

* Implementation note: bootstrap process logic is based on assumption that this contextInitialized * handler is called before servlets initialization. Here is the relevant excerpt from API-DOC: * All ServletContextListeners are notified of context initialization before any filter or servlet in the web application is initialized. * * @param contextEvent context event provided by servlet container. */ @Override public void contextInitialized(ServletContextEvent contextEvent) { final long start = System.currentTimeMillis(); final ServletContext servletContext = contextEvent.getServletContext(); log.debug("Starting application |%s| container...", servletContext.getContextPath()); Enumeration parameterNames = servletContext.getInitParameterNames(); while (parameterNames.hasMoreElements()) { final String name = parameterNames.nextElement(); final String value = servletContext.getInitParameter(name); contextParameters.setProperty(name, value); log.debug("Load context parameter |%s| value |%s|.", name, value); } try { ConfigBuilder builder = new TinyConfigBuilder(servletContext, contextParameters); config(builder.build()); Factory.bind(this); start(); // set tiny container reference on servlet context attribute ONLY if no exception servletContext.setAttribute(TinyContainer.ATTR_INSTANCE, this); log.info("Application |%s| container started in %d msec.", appName, System.currentTimeMillis() - start); } catch (ConfigException e) { log.error(e); log.fatal("Bad container |%s| configuration.", appName); } catch (Throwable t) { log.dump(String.format("Fatal error on container |%s| start:", appName), t); } } /** * Release resources used by this tiny container instance. After execution this method no HTTP requests can be handled. *

* Implementation note: tiny container destruction logic is based on assumption that this * contextDestroyed handler is called after all web application's servlets destruction. Here is the relevant * excerpt from API-DOC: * All servlets and filters have been destroy()ed before any ServletContextListeners are notified of context destruction. * * @param contextEvent context event provided by servlet container. */ @Override public void contextDestroyed(ServletContextEvent contextEvent) { log.debug("Context |%s| destroying.", appName); try { destroy(); } catch (Throwable t) { log.dump(String.format("Fatal error on container |%s| destroy:", appName), t); } } /** * Record session creation to logger trace. * * @param sessionEvent session event provided by servlet container. */ public void sessionCreated(HttpSessionEvent sessionEvent) { log.trace("Create HTTP session |%s|.", sessionEvent.getSession().getId()); } /** * Record session destroying for logger trace. * * @param sessionEvent session event provided by servlet container. */ public void sessionDestroyed(HttpSessionEvent sessionEvent) { log.trace("Destroy HTTP session |%s|.", sessionEvent.getSession().getId()); } // -------------------------------------------------------------------------------------------- // APPLICATION CONTEXT INTERFACE @Override public String getAppName() { return appName; } @Override public File getAppFile(String path) { return new File(privateDir, path); } @Override public String getProperty(String name) { return contextParameters.getProperty(name); } @Override public T getProperty(String name, Class type) { return ConverterRegistry.getConverter().asObject(contextParameters.getProperty(name), type); } @Override public Locale getRequestLocale() { return getInstance(RequestContext.class).getLocale(); } @Override public String getRemoteAddr() { return getInstance(RequestContext.class).getRemoteHost(); } // -------------------------------------------------------------------------------------------- // SECURITY CONTEXT INTERFACE @Override public boolean login(String username, String password) { try { getHttpServletRequest().login(username, password); } catch (ServletException e) { // exception is thrown if request is already authenticated, servlet container authentication is not enabled or // credentials are not accepted // consider all these conditions as login fail but record the event to application logger log.debug(e); return false; } return true; } @Override public void login(Principal user) { final HttpServletRequest request = getHttpServletRequest(); HttpSession session = request.getSession(true); if (user instanceof NonceUser) { final NonceUser nonce = (NonceUser) user; session.setMaxInactiveInterval(nonce.getMaxInactiveInterval()); } try { session.setAttribute(ATTR_PRINCIPAL, user); } catch (IllegalStateException e) { // improbable condition: exception due to invalid session that was just created // it may occur only if another thread temper with login and somehow invalidates the session // while is arguable hard to believe it can theoretically happen an need to be handled // anyway, is not a security breach; if storing principal on session fails, session is not authenticated log.debug(e); } } @Override public void logout() { final HttpServletRequest request = getHttpServletRequest(); try { request.logout(); } catch (ServletException e) { // api-doc is not very explicit about this exception: ' If the logout fails' // swallow this exception but record to application logger log.debug(e); } HttpSession session = request.getSession(false); if (session != null) { // session invalidate takes care to 'unbind any objects bound to it' // but just to be on the safe side remove principal attribute explicitly try { session.removeAttribute(ATTR_PRINCIPAL); session.invalidate(); } catch (IllegalStateException e) { // when enter 'if' block session is valid but could be changed from separated thread // swallow this exception but record to application logger log.debug(e); } } } @SuppressWarnings("unchecked") @Override public T getUserPrincipal() { RequestContext context = getInstance(RequestContext.class); final HttpServletRequest request = context.getRequest(); if (request == null) { log.debug("Attempt to retrieve user principal outside HTTP request."); return null; } // if authentication is provided by servlet container it should be a principal on HTTP request // otherwise it must be a session and on session it must be the principal object // if none from above just return null Principal principal = request.getUserPrincipal(); if (principal != null) { return (T) principal; } HttpSession session = request.getSession(); if (session == null) { return null; } try { return (T) session.getAttribute(ATTR_PRINCIPAL); } catch (IllegalStateException e) { // it can happen session to become invalid from another thread // this is a legal condition; do not even log it to debug return null; } } @Override public boolean isAuthenticated() { return getUserPrincipal() != null; } // -------------------------------------------------------------------------------------------- // CONTAINER SPI @Override public String getLoginRealm() { return loginRealm; } @Override public String getLoginPage() { return loginPage; } @Override public void setProperty(String name, Object value) { if (!(value instanceof String)) { value = ConverterRegistry.getConverter().asString(value); } contextParameters.put(name, value); } // -------------------------------------------------------------------------------------------- // UTILITY METHODS /** * Get HTTP request from current request context. * * @return current HTTP request. * @throws BugError if attempt to use not initialized HTTP request. */ private HttpServletRequest getHttpServletRequest() { RequestContext context = getInstance(RequestContext.class); HttpServletRequest request = context.getRequest(); if (request == null) { throw new BugError("Attempt to use not initialized HTTP request."); } return request; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy