/*
* Copyright 2014 Harald Wellmann.
*
* 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 org.ops4j.pax.web.undertow;
import io.undertow.server.handlers.PathHandler;
import io.undertow.server.handlers.resource.FileResourceManager;
import io.undertow.servlet.Servlets;
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.api.DeploymentManager;
import io.undertow.servlet.api.ErrorPage;
import io.undertow.servlet.api.FilterInfo;
import io.undertow.servlet.api.InstanceFactory;
import io.undertow.servlet.api.ListenerInfo;
import io.undertow.servlet.api.LoginConfig;
import io.undertow.servlet.api.MimeMapping;
import io.undertow.servlet.api.SecurityConstraint;
import io.undertow.servlet.api.SecurityInfo.EmptyRoleSemantic;
import io.undertow.servlet.api.ServletContainerInitializerInfo;
import io.undertow.servlet.api.ServletInfo;
import io.undertow.servlet.api.WebResourceCollection;
import io.undertow.servlet.util.DefaultClassIntrospector;
import io.undertow.servlet.util.ImmediateInstanceFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Dictionary;
import java.util.EventListener;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.Servlet;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import org.ops.pax.web.spi.ServletContainer;
import org.ops.pax.web.spi.ServletContainerInitializerModel;
import org.ops.pax.web.spi.WabModel;
import org.ops.pax.web.spi.WebAppModel;
import org.ops4j.pax.web.descriptor.gen.AuthConstraintType;
import org.ops4j.pax.web.descriptor.gen.AuthMethodType;
import org.ops4j.pax.web.descriptor.gen.ErrorPageType;
import org.ops4j.pax.web.descriptor.gen.FilterMappingType;
import org.ops4j.pax.web.descriptor.gen.FilterType;
import org.ops4j.pax.web.descriptor.gen.FormLoginConfigType;
import org.ops4j.pax.web.descriptor.gen.ListenerType;
import org.ops4j.pax.web.descriptor.gen.LoginConfigType;
import org.ops4j.pax.web.descriptor.gen.MimeMappingType;
import org.ops4j.pax.web.descriptor.gen.ParamValueType;
import org.ops4j.pax.web.descriptor.gen.RoleNameType;
import org.ops4j.pax.web.descriptor.gen.SecurityConstraintType;
import org.ops4j.pax.web.descriptor.gen.ServletMappingType;
import org.ops4j.pax.web.descriptor.gen.ServletNameType;
import org.ops4j.pax.web.descriptor.gen.ServletType;
import org.ops4j.pax.web.descriptor.gen.UrlPatternType;
import org.ops4j.pax.web.descriptor.gen.WarPathType;
import org.ops4j.pax.web.descriptor.gen.WebResourceCollectionType;
import org.ops4j.pax.web.descriptor.gen.WelcomeFileListType;
import org.ops4j.pax.web.undertow.security.IdentityManagerFactory;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.osgi.framework.Version;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implements a {@link ServletContainer} based on Undertow.
* <p>
* On activation, this component starts a HTTP server listening on all interfaces at the port
* indicated by the {@code org.osgi.service.http.port} property, defaulting to 8080.
* <p>
* Deploys and undeploys web applications processed by the web extender.
* <p>
* TODO further configuration properties, access log, more security details
*
* @author Harald Wellmann
*
*/
@Component
public class UndertowServletContainer implements ServletContainer {
private static Logger log = LoggerFactory.getLogger(UndertowServletContainer.class);
private boolean jspPresent;
private IdentityManagerFactory identityManagerFactory;
private UndertowHttpServer httpServer;
@Activate
public void activate(BundleContext ctx) {
// Check if Jastow (JSP support) is available and print a warning otherwise.
// Jastow is an optional dependency.
try {
ctx.getBundle().loadClass("io.undertow.jsp.JspServletBuilder");
jspPresent = true;
}
catch (ClassNotFoundException e) {
log.warn("No runtime support for JSP");
}
}
/**
* Deploys the given web application bundle. An Undertow DeploymentInfo object is built from the
* parsed metadata. The deployment is added to the container, and the context path is added to
* the path handler.
*/
@Override
public void deploy(WabModel webApp) {
Bundle bundle = webApp.getBundle();
WebAppModel webAppModel = webApp.getWebAppModel();
ClassLoader cl = webApp.getClassLoader();
DeploymentInfo deployment = Servlets.deployment().setClassLoader(cl)
.setContextPath(webApp.getContextPath()).setDeploymentName(webApp.getContextPath());
deployment.addServletContextAttribute("osgi-bundlecontext", bundle.getBundleContext());
deployment.addServletContextAttribute("org.ops4j.pax.web.attributes",
new HashMap<String, Object>());
// resource manager for static bundle resources
deployment.setResourceManager(new FileResourceManager(webApp.getExplodedDir(), 4096));
// For bean bundles, we need a class introspector which performs CDI injection.
// For other WABs, we simply create instances using the constructor.
if (webApp.isBeanBundle()) {
deployment.setClassIntrospecter(new LazyCdiClassIntrospector(deployment));
}
else {
deployment.setClassIntrospecter(DefaultClassIntrospector.INSTANCE);
}
addServletContainerInitializers(deployment, webApp);
addInitParameters(deployment, webAppModel);
addServlets(deployment, webApp);
// add the JSP servlet, if Jastow is present
if (jspPresent) {
JspServletFactory.addJspServlet(deployment, webApp);
}
addFilters(deployment, webApp);
addListeners(deployment, webApp);
addWelcomePages(deployment, webApp);
addLoginConfig(deployment, webApp);
addSecurityConstraints(deployment, webApp);
addMimeMappings(deployment, webApp);
addErrorPages(deployment, webApp);
// now deploy the app
io.undertow.servlet.api.ServletContainer servletContainer = Servlets.defaultContainer();
DeploymentManager manager = servletContainer.addDeployment(deployment);
manager.deploy();
try {
String virtualHost = null;
// TODO handle more than one virtual host
if (!webApp.getVirtualHosts().isEmpty()) {
virtualHost = webApp.getVirtualHosts().get(0);
}
PathHandler pathHandler = httpServer.findPathHandlerForHost(virtualHost);
if (pathHandler == null) {
log.error("virtual host [{}] is not defined", virtualHost);
return;
}
pathHandler.addPrefixPath(webApp.getContextPath(), manager.start());
}
catch (ServletException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// register ServletContext service
Dictionary<String, Object> props = new Hashtable<String, Object>();
props.put("osgi.web.contextpath", webApp.getContextPath());
props.put("osgi.web.symbolicname", bundle.getSymbolicName());
if (bundle.getVersion() != Version.emptyVersion) {
props.put("osgi.web.version", bundle.getVersion().toString());
}
ServiceRegistration<ServletContext> registration = bundle.getBundleContext()
.registerService(ServletContext.class, manager.getDeployment().getServletContext(),
props);
webApp.setServletContextRegistration(registration);
}
private void addMimeMappings(DeploymentInfo deployment, WabModel webApp) {
for (MimeMappingType webAppMimeMapping : webApp.getWebAppModel().getMimeMappings()) {
String extension = webAppMimeMapping.getExtension().getValue();
String mimeType = webAppMimeMapping.getMimeType().getValue();
MimeMapping mimeMapping = new MimeMapping(extension, mimeType);
deployment.addMimeMapping(mimeMapping);
}
}
private void addErrorPages(DeploymentInfo deployment, WabModel webApp) {
for (ErrorPageType webAppErrorPage : webApp.getWebAppModel().getErrorPages()) {
String location = webAppErrorPage.getLocation().getValue();
ErrorPage errorPage;
if (webAppErrorPage.getErrorCode() != null) {
int errorCode = webAppErrorPage.getErrorCode().getValue().intValue();
errorPage = new ErrorPage(location, errorCode);
}
else if (webAppErrorPage.getExceptionType() != null) {
String exception = webAppErrorPage.getExceptionType().getValue();
Class<? extends Throwable> klass = loadClass(webApp, exception, Throwable.class);
errorPage = new ErrorPage(location, klass);
}
else {
errorPage = new ErrorPage(location);
}
deployment.addErrorPage(errorPage);
}
}
private void addWelcomePages(DeploymentInfo deployment, WabModel webApp) {
WelcomeFileListType welcomeFileList = webApp.getWebAppModel().getWelcomeFileList();
if (welcomeFileList == null) {
return;
}
for (String welcomeFile : welcomeFileList.getWelcomeFile()) {
deployment.addWelcomePage(welcomeFile);
}
}
private void addServlets(DeploymentInfo deployment, WabModel webApp) {
for (ServletType webAppServlet : webApp.getWebAppModel().getServlets()) {
addServlet(webApp, deployment, webAppServlet);
}
}
private void addInitParameters(DeploymentInfo deployment, WebAppModel webApp) {
for (ParamValueType param : webApp.getContextParams()) {
deployment.addInitParameter(param.getParamName().getValue(), param.getParamValue()
.getValue());
}
}
private void addServletContainerInitializers(DeploymentInfo deployment, WabModel webApp) {
for (ServletContainerInitializerModel wsci : webApp.getServletContainerInitializers()) {
Class<? extends ServletContainerInitializer> sciClass = wsci
.getServletContainerInitializer().getClass();
InstanceFactory<? extends ServletContainerInitializer> instanceFactory = new ImmediateInstanceFactory<>(
wsci.getServletContainerInitializer());
// TODO find classes handled by initializer
ServletContainerInitializerInfo sciInfo = new ServletContainerInitializerInfo(sciClass,
instanceFactory, new HashSet<Class<?>>());
deployment.addServletContainerInitalizer(sciInfo);
}
}
private void addServlet(WabModel webApp, DeploymentInfo deployment, ServletType webAppServlet) {
String servletName = webAppServlet.getServletName().getValue();
Class<? extends Servlet> servletClass = loadClass(webApp, webAppServlet.getServletClass()
.getValue(), Servlet.class);
ServletInfo servletInfo;
if (webApp.isBeanBundle()) {
try {
servletInfo = Servlets.servlet(servletName, servletClass, deployment
.getClassIntrospecter().createInstanceFactory(servletClass));
}
catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return;
}
}
else {
servletInfo = Servlets.servlet(servletName, servletClass);
}
for (ServletMappingType servletMapping : webApp.getWebAppModel().getServletMappings()) {
if (servletMapping.getServletName().getValue().equals(servletName)) {
for (UrlPatternType urlPattern : servletMapping.getUrlPattern()) {
servletInfo.addMapping(urlPattern.getValue());
}
}
}
deployment.addServlet(servletInfo);
}
private <T> Class<? extends T> loadClass(WabModel webApp, String className, Class<T> baseClass) {
try {
@SuppressWarnings("unchecked")
Class<? extends T> klass = (Class<? extends T>) webApp.getClassLoader().loadClass(
className);
return klass;
}
catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
}
}
private void addFilters(DeploymentInfo deployment, WabModel webApp) {
WebAppModel webAppModel = webApp.getWebAppModel();
for (FilterType filter : webAppModel.getFilters()) {
addFilter(webApp, deployment, filter);
}
String filterName = "OSGi Protected Dirs";
FilterInfo filterInfo = Servlets.filter(filterName, ProtectedDirectoryFilter.class);
deployment.addFilter(filterInfo);
Collection<DispatcherType> dispatcherTypes = getDispatcherTypes(null);
for (DispatcherType dispatcherType : dispatcherTypes) {
deployment.addFilterUrlMapping(filterName, "/*", dispatcherType);
}
}
private void addFilter(WabModel webApp, DeploymentInfo deployment, FilterType webAppFilter) {
String filterName = webAppFilter.getFilterName().getValue();
Class<? extends Filter> filterClass = loadClass(webApp, filterName, Filter.class);
FilterInfo filterInfo;
if (webApp.isBeanBundle()) {
filterInfo = Servlets.filter(filterName, filterClass, new LazyCdiInstanceFactory<>(
deployment, filterClass));
}
else {
filterInfo = Servlets.filter(filterName, filterClass);
}
deployment.addFilter(filterInfo);
for (FilterMappingType filterMapping : webApp.getWebAppModel().getFilterMappings()) {
if (filterMapping.getFilterName().getValue().equals(filterName)) {
Collection<DispatcherType> dispatcherTypes = getDispatcherTypes(filterMapping
.getDispatcher());
for (Object obj : filterMapping.getUrlPatternOrServletName()) {
if (obj instanceof ServletNameType) {
ServletNameType servletName = (ServletNameType) obj;
for (DispatcherType dispatcherType : dispatcherTypes) {
deployment.addFilterServletNameMapping(filterName,
servletName.getValue(), dispatcherType);
}
}
else if (obj instanceof UrlPatternType) {
UrlPatternType urlPattern = (UrlPatternType) obj;
for (DispatcherType dispatcherType : dispatcherTypes) {
deployment.addFilterUrlMapping(filterName, urlPattern.getValue(),
dispatcherType);
}
}
}
}
}
}
private Collection<DispatcherType> getDispatcherTypes(
List<org.ops4j.pax.web.descriptor.gen.DispatcherType> dispatcherTypes) {
List<DispatcherType> result = new ArrayList<>();
if (dispatcherTypes == null || dispatcherTypes.isEmpty()) {
result.add(DispatcherType.REQUEST);
}
else {
for (org.ops4j.pax.web.descriptor.gen.DispatcherType dispatcherType : dispatcherTypes) {
result.add(DispatcherType.valueOf(dispatcherType.getValue()));
}
}
return result;
}
private void addListeners(DeploymentInfo deployment, WabModel webApp) {
for (ListenerType webAppListener : webApp.getWebAppModel().getListeners()) {
ListenerInfo listenerInfo;
Class<? extends EventListener> listenerClass = loadClass(webApp, webAppListener
.getListenerClass().getValue(), EventListener.class);
if (webApp.isBeanBundle()) {
listenerInfo = Servlets.listener(listenerClass, new LazyCdiInstanceFactory<>(
deployment, listenerClass));
}
else {
listenerInfo = Servlets.listener(listenerClass);
}
deployment.addListener(listenerInfo);
}
}
private void addLoginConfig(DeploymentInfo deployment, WabModel webApp) {
LoginConfigType webAppLoginConfig = webApp.getWebAppModel().getLoginConfig();
if (webAppLoginConfig == null) {
return;
}
String realmName = webAppLoginConfig.getRealmName().getValue();
FormLoginConfigType formLoginConfig = webAppLoginConfig.getFormLoginConfig();
String loginPage = null;
String errorPage = null;
if (formLoginConfig != null) {
WarPathType flp = formLoginConfig.getFormLoginPage();
WarPathType fep = formLoginConfig.getFormErrorPage();
loginPage = flp == null ? null : flp.getValue();
errorPage = fep == null ? null : fep.getValue();
}
AuthMethodType authMethod = webAppLoginConfig.getAuthMethod();
LoginConfig loginConfig = new LoginConfig(
authMethod == null ? null : authMethod.getValue(), realmName, loginPage, errorPage);
deployment.setLoginConfig(loginConfig);
deployment.setIdentityManager(identityManagerFactory
.createIdentityManagerFactory(realmName));
}
private void addSecurityConstraints(DeploymentInfo deployment, WabModel webApp) {
for (SecurityConstraintType constraint : webApp.getWebAppModel().getSecurityConstraints()) {
addSecurityConstraint(deployment, constraint);
}
}
private void addSecurityConstraint(DeploymentInfo deployment, SecurityConstraintType constraint) {
SecurityConstraint securityConstraint = new SecurityConstraint();
List<String> roles = new ArrayList<>();
AuthConstraintType authConstraint = constraint.getAuthConstraint();
if (authConstraint != null) {
for (RoleNameType roleName : authConstraint.getRoleName()) {
roles.add(roleName.getValue());
}
}
securityConstraint.addRolesAllowed(roles);
securityConstraint.setEmptyRoleSemantic(EmptyRoleSemantic.PERMIT);
for (WebResourceCollectionType wrcSource : constraint.getWebResourceCollection()) {
WebResourceCollection wrc = new WebResourceCollection();
for (UrlPatternType urlPattern : wrcSource.getUrlPattern()) {
wrc.addUrlPattern(urlPattern.getValue());
}
securityConstraint.addWebResourceCollection(wrc);
}
deployment.addSecurityConstraint(securityConstraint);
}
/**
* Undeploys the given web application bundle. Stops the deployment and removes the context path
* from the path handler.
*/
@Override
public void undeploy(WabModel webApp) {
ServiceRegistration<ServletContext> registration = webApp.getServletContextRegistration();
registration.unregister();
io.undertow.servlet.api.ServletContainer servletContainer = Servlets.defaultContainer();
DeploymentManager manager = servletContainer.getDeploymentByPath(webApp.getContextPath());
httpServer.getPathHandler().removePrefixPath(webApp.getContextPath());
try {
manager.stop();
}
catch (ServletException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
manager.undeploy();
}
@Reference
public void setIdentityManager(IdentityManagerFactory identityManagerFactory) {
this.identityManagerFactory = identityManagerFactory;
}
@Reference
public void setHttpServer(UndertowHttpServer httpServer) {
this.httpServer = httpServer;
}
}