/*
* JSmart Framework - Java Web Development Framework
* Copyright (c) 2015, Jeferson Albino da Silva, All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
package com.jsmartframework.web.manager;
import static com.jsmartframework.web.config.Config.CONFIG;
import static com.jsmartframework.web.manager.BeanHandler.HANDLER;
import static com.jsmartframework.web.util.WebImage.IMAGES;
import static com.jsmartframework.web.util.WebText.TEXTS;
import com.jsmartframework.web.annotation.WebFilter;
import com.jsmartframework.web.annotation.WebServlet;
import com.jsmartframework.web.config.Constants;
import com.jsmartframework.web.config.InitParam;
import com.jsmartframework.web.config.SecureMethod;
import com.jsmartframework.web.config.UploadConfig;
import com.jsmartframework.web.config.UrlPattern;
import org.springframework.web.context.ContextLoader;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.EventListener;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterRegistration;
import javax.servlet.HttpConstraintElement;
import javax.servlet.HttpMethodConstraintElement;
import javax.servlet.MultipartConfigElement;
import javax.servlet.Servlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletRegistration;
import javax.servlet.ServletSecurityElement;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.ServletSecurity.EmptyRoleSemantic;
import javax.servlet.annotation.ServletSecurity.TransportGuarantee;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebListener;
@WebListener
public final class ContextControl implements ServletContextListener {
private static final List<String> METHODS = Arrays.asList("GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "TRACE");
private static ContextLoader CONTEXT_LOADER;
@Override
@SuppressWarnings("unchecked")
public void contextInitialized(ServletContextEvent event) {
try {
ServletContext servletContext = event.getServletContext();
CONFIG.init(servletContext);
if (CONFIG.getContent() == null) {
throw new RuntimeException("Configuration file " + Constants.WEB_CONFIG_XML + " was not found in WEB-INF resources folder!");
}
String contextConfigLocation = "com.jsmartframework.web.manager";
if (CONFIG.getContent().getPackageScan() != null) {
contextConfigLocation += "," + CONFIG.getContent().getPackageScan();
}
// Configure necessary parameters in the ServletContext to set Spring configuration without needing an XML file
AnnotationConfigWebApplicationContext configWebAppContext = new AnnotationConfigWebApplicationContext();
configWebAppContext.setConfigLocation(contextConfigLocation);
CONTEXT_LOADER = new ContextLoader(configWebAppContext);
CONTEXT_LOADER.initWebApplicationContext(servletContext);
TagEncrypter.init();
TEXTS.init();
IMAGES.init(servletContext);
HANDLER.init(servletContext);
// ServletControl -> @MultipartConfig @WebServlet(name = "ServletControl", displayName = "ServletControl", loadOnStartup = 1)
Servlet servletControl = servletContext.createServlet((Class<? extends Servlet>) Class.forName("com.jsmartframework.web.manager.ServletControl"));
ServletRegistration.Dynamic servletControlReg = (ServletRegistration.Dynamic) servletContext.addServlet("ServletControl", servletControl);
servletControlReg.setAsyncSupported(true);
servletControlReg.setLoadOnStartup(1);
// ServletControl Initial Parameters
InitParam[] initParams = CONFIG.getContent().getInitParams();
if (initParams != null) {
for (InitParam initParam : initParams) {
servletControlReg.setInitParameter(initParam.getName(), initParam.getValue());
}
}
// MultiPart to allow file upload on ServletControl
MultipartConfigElement multipartElement = getServletMultipartElement();
if (multipartElement != null) {
servletControlReg.setMultipartConfig(multipartElement);
}
// Security constraint to ServletControl
ServletSecurityElement servletSecurityElement = getServletSecurityElement(servletContext);
if (servletSecurityElement != null) {
servletControlReg.setServletSecurity(servletSecurityElement);
}
// TODO: Fix problem related to authentication by container to use SSL dynamically (Maybe create more than one servlet for secure and non-secure patterns)
// Check also the use of request.login(user, pswd)
// Check the HttpServletRequest.BASIC_AUTH, CLIENT_CERT_AUTH, FORM_AUTH, DIGEST_AUTH
// servletReg.setRunAsRole("admin");
// servletContext.declareRoles("admin");
// ServletControl URL mapping
String[] servletMapping = getServletMapping();
servletControlReg.addMapping(servletMapping);
// ErrorFilter -> @WebFilter(urlPatterns = {"/*"})
Filter errorFilter = servletContext.createFilter((Class<? extends Filter>) Class.forName("com.jsmartframework.web.filter.ErrorFilter"));
FilterRegistration.Dynamic errorFilterReg = (FilterRegistration.Dynamic) servletContext.addFilter("ErrorFilter", errorFilter);
errorFilterReg.setAsyncSupported(true);
errorFilterReg.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.ERROR), true, "/*");
// EncodeFilter -> @WebFilter(urlPatterns = {"/*"})
Filter encodeFilter = servletContext.createFilter((Class<? extends Filter>) Class.forName("com.jsmartframework.web.filter.EncodeFilter"));
FilterRegistration.Dynamic encodeFilterReg = (FilterRegistration.Dynamic) servletContext.addFilter("EncodeFilter", encodeFilter);
encodeFilterReg.setAsyncSupported(true);
encodeFilterReg.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR), true, "/*");
// CacheFilter -> @WebFilter(urlPatterns = {"/*"})
Filter cacheFilter = servletContext.createFilter((Class<? extends Filter>) Class.forName("com.jsmartframework.web.filter.CacheFilter"));
FilterRegistration.Dynamic cacheFilterReg = (FilterRegistration.Dynamic) servletContext.addFilter("CacheFilter", cacheFilter);
cacheFilterReg.setAsyncSupported(true);
cacheFilterReg.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR), true, "/*");
// Add custom filters defined by client
for (String filterName : sortCustomFilters()) {
Filter customFilter = servletContext.createFilter((Class<? extends Filter>) HANDLER.webFilters.get(filterName));
HANDLER.executeInjection(customFilter);
WebFilter webFilter = customFilter.getClass().getAnnotation(WebFilter.class);
FilterRegistration.Dynamic customFilterReg = (FilterRegistration.Dynamic) servletContext.addFilter(filterName, customFilter);
if (webFilter.initParams() != null) {
for (WebInitParam initParam : webFilter.initParams()) {
customFilterReg.setInitParameter(initParam.name(), initParam.value());
}
}
customFilterReg.setAsyncSupported(webFilter.asyncSupported());
customFilterReg.addMappingForUrlPatterns(EnumSet.copyOf(Arrays.asList(webFilter.dispatcherTypes())), true,
webFilter.urlPatterns());
}
// FilterControl -> @WebFilter(servletNames = {"ServletControl"})
Filter filterControl = servletContext.createFilter((Class<? extends Filter>) Class.forName("com.jsmartframework.web.manager.FilterControl"));
FilterRegistration.Dynamic filterControlReg = (FilterRegistration.Dynamic) servletContext.addFilter("FilterControl", filterControl);
filterControlReg.setAsyncSupported(true);
filterControlReg.addMappingForServletNames(EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.ERROR,
DispatcherType.INCLUDE), true, "ServletControl");
// OutputFilter -> @WebFilter(servletNames = {"ServletControl"})
Filter outputFilter = servletContext.createFilter((Class<? extends Filter>) Class.forName("com.jsmartframework.web.manager.OutputFilter"));
FilterRegistration.Dynamic outputFilterReg = (FilterRegistration.Dynamic) servletContext.addFilter("OutputFilter", outputFilter);
outputFilterReg.setAsyncSupported(true);
outputFilterReg.addMappingForServletNames(EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.ERROR,
DispatcherType.INCLUDE), true, "ServletControl");
// AsyncFilter -> @WebFilter(servletNames = {"ServletControl"})
// Filter used case AsyncContext is dispatched internally by AsyncBean implementation
Filter asyncFilter = servletContext.createFilter((Class<? extends Filter>) Class.forName("com.jsmartframework.web.manager.AsyncFilter"));
FilterRegistration.Dynamic asyncFilterReg = (FilterRegistration.Dynamic) servletContext.addFilter("AsyncFilter", asyncFilter);
asyncFilterReg.setAsyncSupported(true);
asyncFilterReg.addMappingForServletNames(EnumSet.of(DispatcherType.ASYNC), true, "ServletControl");
// SessionControl -> @WebListener
EventListener sessionListener = servletContext.createListener((Class<? extends EventListener>) Class.forName("com.jsmartframework.web.manager.SessionControl"));
servletContext.addListener(sessionListener);
// RequestControl -> @WebListener
EventListener requestListener = servletContext.createListener((Class<? extends EventListener>) Class.forName("com.jsmartframework.web.manager.RequestControl"));
servletContext.addListener(requestListener);
// Custom WebServlet -> Custom Servlets created by application
for (String servletName : HANDLER.webServlets.keySet()) {
Servlet customServlet = servletContext.createServlet((Class<? extends Servlet>) HANDLER.webServlets.get(servletName));
HANDLER.executeInjection(customServlet);
WebServlet webServlet = customServlet.getClass().getAnnotation(WebServlet.class);
ServletRegistration.Dynamic customReg = (ServletRegistration.Dynamic) servletContext.addServlet(servletName, customServlet);
customReg.setLoadOnStartup(webServlet.loadOnStartup());
customReg.setAsyncSupported(webServlet.asyncSupported());
WebInitParam[] customInitParams = webServlet.initParams();
if (customInitParams != null) {
for (WebInitParam customInitParam : customInitParams) {
customReg.setInitParameter(customInitParam.name(), customInitParam.value());
}
}
// Add mapping url for custom servlet
customReg.addMapping(webServlet.urlPatterns());
if (customServlet.getClass().isAnnotationPresent(MultipartConfig.class)) {
customReg.setMultipartConfig(new MultipartConfigElement(customServlet.getClass().getAnnotation(MultipartConfig.class)));
}
}
// Controller Dispatcher for Spring MVC
Set<String> requestPaths = HANDLER.requestPaths.keySet();
if (!requestPaths.isEmpty()) {
ServletRegistration.Dynamic mvcDispatcherReg = servletContext.addServlet("DispatcherServlet", new DispatcherServlet(configWebAppContext));
mvcDispatcherReg.setLoadOnStartup(1);
mvcDispatcherReg.addMapping(requestPaths.toArray(new String[requestPaths.size()]));
// RequestPathFilter -> @WebFilter(servletNames = {"DispatcherServlet"})
Filter requestPathFilter = servletContext.createFilter((Class<? extends Filter>) Class.forName("com.jsmartframework.web.manager.RequestPathFilter"));
FilterRegistration.Dynamic reqPathFilterReg = (FilterRegistration.Dynamic) servletContext.addFilter("RequestPathFilter", requestPathFilter);
reqPathFilterReg.addMappingForServletNames(EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD,
DispatcherType.ERROR, DispatcherType.INCLUDE, DispatcherType.ASYNC), true, "DispatcherServlet");
}
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
@Override
public void contextDestroyed(ServletContextEvent event) {
HANDLER.destroy(event.getServletContext());
CONTEXT_LOADER.closeWebApplicationContext(event.getServletContext());
}
private List<String> sortCustomFilters() {
// Sort the custom filter by the order specified
List<String> customFilters = new ArrayList<String>(HANDLER.webFilters.keySet());
Collections.sort(customFilters, new Comparator<String>() {
@Override
public int compare(String filterNameOne, String filterNameTwo) {
WebFilter webFilterOne = HANDLER.webFilters.get(filterNameOne).getAnnotation(WebFilter.class);
WebFilter webFilterTwo = HANDLER.webFilters.get(filterNameTwo).getAnnotation(WebFilter.class);
return Integer.compare(webFilterOne.order(), webFilterTwo.order());
}
});
return customFilters;
}
private MultipartConfigElement getServletMultipartElement() {
UploadConfig uploadConfig = null;
MultipartConfigElement multipartElement = new MultipartConfigElement("");
if ((uploadConfig = CONFIG.getContent().getUploadConfig()) != null) {
multipartElement = new MultipartConfigElement(uploadConfig.getLocation(), uploadConfig.getMaxFileSize(), uploadConfig.getMaxRequestSize(), uploadConfig.getFileSizeThreshold());
}
return multipartElement;
}
private ServletSecurityElement getServletSecurityElement(ServletContext servletContext) {
SecureMethod[] secureMethods = CONFIG.getContent().getSecureMethods();
if (secureMethods != null && secureMethods.length > 0) {
HttpConstraintElement constraint = new HttpConstraintElement();
SecureMethod allMethods = CONFIG.getContent().getSecureMethod("*");
Set<HttpMethodConstraintElement> methodConstraints = new HashSet<HttpMethodConstraintElement>();
if (allMethods != null) {
for (String method : METHODS) {
HttpConstraintElement constraintElement = getHttpConstraintElement(allMethods);
if (constraintElement != null) {
methodConstraints.add(new HttpMethodConstraintElement(method, constraintElement));
}
}
} else {
for (SecureMethod secureMethod : secureMethods) {
HttpConstraintElement constraintElement = getHttpConstraintElement(secureMethod);
if (constraintElement != null) {
if (secureMethod.getMethod() == null || !METHODS.contains(secureMethod.getMethod().toUpperCase())) {
throw new RuntimeException("Method name declared in [secure-method] tag is unsupported! Supported values are HTTP methods.");
}
methodConstraints.add(new HttpMethodConstraintElement(secureMethod.getMethod().toUpperCase(), constraintElement));
}
}
}
return new ServletSecurityElement(constraint, methodConstraints);
}
return null;
}
private HttpConstraintElement getHttpConstraintElement(SecureMethod secureMethod) {
HttpConstraintElement constraintElement = null;
if (secureMethod.getEmptyRole() != null && secureMethod.getTransport() != null) {
EmptyRoleSemantic emptyRole = getEmptyRoleSemantic(secureMethod.getEmptyRole());
TransportGuarantee transport = getTransportGuarantee(secureMethod.getTransport());
if (transport == null || emptyRole == null) {
throw new RuntimeException("Invalid transport or emptyRole attribute for [secure-method] tag! Values allowed are [confidential, none].");
}
constraintElement = new HttpConstraintElement(emptyRole, transport, secureMethod.getRoles() != null ? secureMethod.getRoles() : new String[]{});
} else if (secureMethod.getTransport() != null) {
TransportGuarantee transport = getTransportGuarantee(secureMethod.getTransport());
if (transport == null) {
throw new RuntimeException("Invalid transport attribute for [secure-method] tag! Values allowed are [confidential, none].");
}
constraintElement = new HttpConstraintElement(transport, secureMethod.getRoles() != null ? secureMethod.getRoles() : new String[]{});
} else if (secureMethod.getEmptyRole() != null) {
EmptyRoleSemantic emptyRole = getEmptyRoleSemantic(secureMethod.getEmptyRole());
if (emptyRole == null) {
throw new RuntimeException("Invalid emptyRole attribute for [secure-method] tag! Values allowed are [deny, permit].");
}
constraintElement = new HttpConstraintElement(emptyRole);
}
return constraintElement;
}
private TransportGuarantee getTransportGuarantee(String transport) {
return transport.equalsIgnoreCase("confidential") ? TransportGuarantee.CONFIDENTIAL : transport.equalsIgnoreCase("none") ? TransportGuarantee.NONE : null;
}
private EmptyRoleSemantic getEmptyRoleSemantic(String emptyRole) {
return emptyRole.equalsIgnoreCase("deny") ? EmptyRoleSemantic.DENY : emptyRole.equalsIgnoreCase("permit") ? EmptyRoleSemantic.PERMIT : null;
}
private String[] getServletMapping() {
List<String> mapping = new ArrayList<String>();
if (CONFIG.getContent().getUrlPatterns() == null) {
throw new RuntimeException("None [url-pattern] tags were found in configuration file " + Constants.WEB_CONFIG_XML
+ " for url mapping! At lease one URL pattern must be informed.");
}
for (UrlPattern urlPattern : CONFIG.getContent().getUrlPatterns()) {
mapping.add(urlPattern.getUrl());
}
CONFIG.addMappedUrls(mapping);
return mapping.toArray(new String[mapping.size()]);
}
}