js.servlet.TinyContainer Maven / Gradle / Ivy
Show all versions of tiny-container Show documentation
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;
}
}