/*
* JBoss, Home of Professional Open Source.
* Copyright 2008, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This 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 2.1 of
* the License, or (at your option) any later version.
*
* This software 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 software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.web.tomcat.service.deployers;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.security.CodeSource;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipFile;
import javax.management.Attribute;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.LinkRef;
import org.apache.catalina.Container;
import org.apache.catalina.Engine;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Loader;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.modeler.Registry;
import org.jboss.deployers.spi.DeploymentException;
import org.jboss.deployers.structure.spi.DeploymentUnit;
import org.jboss.deployers.vfs.spi.structure.VFSDeploymentUnit;
import org.jboss.logging.Logger;
import org.jboss.metadata.web.jboss.JBossWebMetaData;
import org.jboss.mx.util.MBeanServerLocator;
import org.jboss.naming.NonSerializableFactory;
import org.jboss.security.SecurityUtil;
import org.jboss.virtual.VirtualFile;
import org.jboss.web.WebApplication;
import org.jboss.web.deployers.AbstractWarDeployment;
import org.jboss.web.tomcat.security.JaccContextValve;
import org.jboss.web.tomcat.security.RunAsListener;
import org.jboss.web.tomcat.security.SecurityAssociationValve;
import org.jboss.web.tomcat.security.SecurityContextEstablishmentValve;
import org.jboss.web.tomcat.service.TomcatInjectionContainer;
import org.jboss.web.tomcat.service.WebCtxLoader;
import org.jboss.web.tomcat.service.request.ActiveRequestResponseCacheValve;
import org.jboss.web.tomcat.service.session.AbstractJBossManager;
import org.jboss.web.tomcat.service.session.distributedcache.spi.ClusteringNotSupportedException;
import org.omg.CORBA.ORB;
/**
* A tomcat web application deployment.
*
* @author Scott.Stark@jboss.org
* @author Costin Manolache
* @author adrian@jboss.org
* @version $Revision: 102458 $
*/
public class TomcatDeployment extends AbstractWarDeployment
{
private static final Logger log = Logger.getLogger(TomcatDeployment.class);
/**
* The name of the war level context configuration descriptor
*/
private static final String CONTEXT_CONFIG_FILE = "WEB-INF/context.xml";
private DeployerConfig config;
private final String[] javaVMs = { " jboss.management.local:J2EEServer=Local,j2eeType=JVM,name=localhost" };
private final String serverName = "jboss";
private final HashMap vhostToHostNames = new HashMap();
private ORB orb = null;
public ORB getORB()
{
return orb;
}
public void setORB(ORB orb)
{
this.orb = orb;
}
@Override
public void init(Object containerConfig) throws Exception
{
this.config = (DeployerConfig)containerConfig;
super.setJava2ClassLoadingCompliance(config.isJava2ClassLoadingCompliance());
super.setUnpackWars(config.isUnpackWars());
super.setLenientEjbLink(config.isLenientEjbLink());
super.setDefaultSecurityDomain(config.getDefaultSecurityDomain());
}
@Override
protected void performDeploy(WebApplication webApp, String warUrl) throws Exception
{
// Decode the URL as tomcat can't deal with paths with escape chars
warUrl = URLDecoder.decode(warUrl, "UTF-8");
webApp.setDomain(config.getCatalinaDomain());
JBossWebMetaData metaData = webApp.getMetaData();
String hostName = null;
// Get any jboss-web/virtual-hosts
List<String> vhostNames = metaData.getVirtualHosts();
// Map the virtual hosts onto the configured hosts
Iterator hostNames = mapVirtualHosts(vhostNames);
if (hostNames.hasNext())
{
hostName = hostNames.next().toString();
}
else
{
hostNames = getDefaultHosts();
if (hostNames.hasNext())
{
hostName = hostNames.next().toString();
}
}
performDeployInternal(webApp, hostName, warUrl);
while (hostNames.hasNext())
{
String additionalHostName = hostNames.next().toString();
performDeployInternal(webApp, additionalHostName, warUrl);
}
}
protected void performDeployInternal(WebApplication webApp, String hostName, String warUrlStr) throws Exception
{
JBossWebMetaData metaData = webApp.getMetaData();
String ctxPath = metaData.getContextRoot();
if (ctxPath.equals("/") || ctxPath.equals("/ROOT") || ctxPath.equals(""))
{
log.debug("deploy root context=" + ctxPath);
ctxPath = "/";
metaData.setContextRoot(ctxPath);
}
log.info("deploy, ctxPath=" + ctxPath);
URL warUrl = new URL(warUrlStr);
ClassLoader loader = Thread.currentThread().getContextClassLoader();
metaData.setContextLoader(loader);
StandardContext context = (StandardContext)Class.forName(config.getContextClassName()).newInstance();
DeploymentUnit depUnit = webApp.getDeploymentUnit();
TomcatInjectionContainer injectionContainer = new TomcatInjectionContainer(webApp, depUnit, context, getPersistenceUnitDependencyResolver());
Loader webLoader = depUnit.getAttachment(Loader.class);
if (webLoader == null)
webLoader = getWebLoader(depUnit, metaData, loader, warUrl, injectionContainer);
webApp.setName(warUrl.getPath());
webApp.setClassLoader(loader);
webApp.setURL(warUrl);
String objectNameS = config.getCatalinaDomain() + ":j2eeType=WebModule,name=//" + ((hostName == null) ? "localhost" : hostName) + ctxPath
+ ",J2EEApplication=none,J2EEServer=none";
ObjectName objectName = new ObjectName(objectNameS);
if (Registry.getRegistry(null, null).getMBeanServer().isRegistered(objectName))
throw new DeploymentException("Web mapping already exists for deployment URL " + warUrlStr);
Registry.getRegistry(null, null).registerComponent(context, objectName, config.getContextClassName());
context.setConfigFile(CONTEXT_CONFIG_FILE);
context.setInstanceManager(injectionContainer);
context.setDefaultContextXml("context.xml");
context.setDefaultWebXml("conf/web.xml");
context.setPublicId(metaData.getPublicID());
String docBase = depUnit.getAttachment("org.jboss.web.explicitDocBase", String.class);
if (docBase == null)
docBase = warUrl.getFile();
context.setDocBase(docBase);
// If there is an alt-dd set it
if (metaData.getAlternativeDD() != null)
{
log.debug("Setting altDDName to: " + metaData.getAlternativeDD());
context.setAltDDName(metaData.getAlternativeDD());
}
context.setJavaVMs(javaVMs);
context.setServer(serverName);
context.setSaveConfig(false);
if (webLoader != null)
{
context.setLoader(webLoader);
}
else
{
context.setParentClassLoader(loader);
}
context.setDelegate(webApp.getJava2ClassLoadingCompliance());
// Javac compatibility whenever possible
String[] jspCP = getCompileClasspath(loader);
StringBuffer classpath = new StringBuffer();
for (int u = 0; u < jspCP.length; u++)
{
String repository = jspCP[u];
if (repository == null)
continue;
if (repository.startsWith("file://"))
repository = repository.substring(7);
else if (repository.startsWith("file:"))
repository = repository.substring(5);
else
continue;
if (repository == null)
continue;
// ok it is a file. Make sure that is is a directory or jar file
File fp = new File(repository);
if (!fp.isDirectory())
{
// if it is not a directory, try to open it as a zipfile.
try
{
// avoid opening .xml files
if (fp.getName().toLowerCase().endsWith(".xml"))
continue;
ZipFile zip = new ZipFile(fp);
zip.close();
}
catch (IOException e)
{
continue;
}
}
if (u > 0)
classpath.append(File.pathSeparator);
classpath.append(repository);
}
context.setCompilerClasspath(classpath.toString());
// Set the session cookies flag according to metadata
switch (metaData.getSessionCookies())
{
case JBossWebMetaData.SESSION_COOKIES_ENABLED:
context.setCookies(true);
log.debug("Enabling session cookies");
break;
case JBossWebMetaData.SESSION_COOKIES_DISABLED:
context.setCookies(false);
log.debug("Disabling session cookies");
break;
default:
log.debug("Using session cookies default setting");
}
String metaDataSecurityDomain = metaData.getSecurityDomain();
if (metaDataSecurityDomain != null)
metaDataSecurityDomain = metaDataSecurityDomain.trim();
//Add a valve to cache the active request/response
Engine engine = this.getCatalinaEngine(context);
if(engine != null)
{
ActiveRequestResponseCacheValve activeReqValve = new ActiveRequestResponseCacheValve();
engine.getPipeline().addValve(activeReqValve);
}
// Add a valve to establish security context
SecurityContextEstablishmentValve scevalve = new SecurityContextEstablishmentValve(metaDataSecurityDomain, SecurityUtil.unprefixSecurityDomain(config
.getDefaultSecurityDomain()), SecurityActions.loadClass(config.getSecurityContextClassName()), getSecurityManagement());
context.addValve(scevalve);
// Add a valve to estalish the JACC context before authorization valves
Certificate[] certs = null;
CodeSource cs = new CodeSource(warUrl, certs);
JaccContextValve jaccValve = new JaccContextValve(metaData, cs);
context.addValve(jaccValve);
// Set listener
context.setConfigClass("org.jboss.web.tomcat.service.deployers.JBossContextConfig");
context.addLifecycleListener(new EncListener(loader, webLoader, injectionContainer, webApp));
// Pass the metadata to the RunAsListener via a thread local
RunAsListener.metaDataLocal.set(metaData);
JBossContextConfig.metaDataLocal.set(metaData);
JBossContextConfig.metaDataShared.set(config.getSharedMetaData());
JBossContextConfig.deployerConfig.set(config);
JBossContextConfig.kernelLocal.set(kernel);
JBossContextConfig.deploymentUnitLocal.set(unit);
try
{
// Start it
context.start();
// Build the ENC
}
catch (Exception e)
{
context.destroy();
DeploymentException.rethrowAsDeploymentException("URL " + warUrlStr + " deployment failed", e);
}
finally
{
RunAsListener.metaDataLocal.set(null);
JBossContextConfig.metaDataLocal.set(null);
JBossContextConfig.metaDataShared.set(null);
JBossContextConfig.deployerConfig.set(null);
JBossContextConfig.kernelLocal.set(null);
JBossContextConfig.deploymentUnitLocal.set(null);
}
if (context.getState() != 1)
{
context.destroy();
throw new DeploymentException("URL " + warUrlStr + " deployment failed");
}
// Clustering
if (config.getOverrideDistributableManager() && metaData.getDistributable() != null)
{
// Try to initate clustering, fallback to standard if no clustering is
// available
try
{
AbstractJBossManager manager = null;
String managerClassName = config.getManagerClass();
Class managerClass = Thread.currentThread().getContextClassLoader().loadClass(managerClassName);
manager = (AbstractJBossManager)managerClass.newInstance();
String name = "//" + ((hostName == null) ? "localhost" : hostName) + ctxPath;
manager.init(name, metaData);
server.setAttribute(objectName, new Attribute("manager", manager));
log.debug("Enabled clustering support for ctxPath=" + ctxPath);
}
catch (ClusteringNotSupportedException e)
{
// JBAS-3513 Just log a WARN, not an ERROR
log.warn("Failed to setup clustering, clustering disabled. ClusteringNotSupportedException: " + e.getMessage());
}
catch (NoClassDefFoundError ncdf)
{
// JBAS-3513 Just log a WARN, not an ERROR
log.debug("Classes needed for clustered webapp unavailable", ncdf);
log.warn("Failed to setup clustering, clustering disabled. NoClassDefFoundError: " + ncdf.getMessage());
}
catch (Throwable t)
{
// TODO consider letting this through and fail the deployment
log.error("Failed to setup clustering, clustering disabled. Exception: ", t);
}
}
/*
* Add security association valve after the authorization valves so that the authenticated user may be associated
* with the request thread/session.
*/
SecurityAssociationValve valve = new SecurityAssociationValve(metaData, config.getSecurityManagerService());
valve.setSubjectAttributeName(config.getSubjectAttributeName());
server.invoke(objectName, "addValve", new Object[] { valve }, new String[] { "org.apache.catalina.Valve" });
/*
* TODO: Retrieve the state, and throw an exception in case of a failure Integer state = (Integer)
* server.getAttribute(objectName, "state"); if (state.intValue() != 1) { throw new DeploymentException("URL " +
* warUrl + " deployment failed"); }
*/
webApp.setAppData(objectName);
/*
* TODO: Create mbeans for the servlets ObjectName servletQuery = new ObjectName (config.getCatalinaDomain() +
* ":j2eeType=Servlet,WebModule=" + objectName.getKeyProperty("name") + ",*"); Iterator iterator =
* server.queryMBeans(servletQuery, null).iterator(); while (iterator.hasNext()) {
* di.mbeans.add(((ObjectInstance)iterator.next()).getObjectName()); }
*/
log.debug("Initialized: " + webApp + " " + objectName);
}
public class EncListener implements LifecycleListener
{
protected ClassLoader loader;
protected Loader webLoader;
protected WebApplication webApp;
protected JBossWebMetaData metaData;
protected DeploymentUnit unit;
protected TomcatInjectionContainer injectionContainer;
public EncListener(ClassLoader loader, Loader webLoader, TomcatInjectionContainer injectionContainer, WebApplication webApp)
{
this.loader = loader;
this.webLoader = webLoader;
this.injectionContainer = injectionContainer;
this.webApp = webApp;
this.metaData = webApp.getMetaData();
this.unit = webApp.getDeploymentUnit();
}
public void lifecycleEvent(LifecycleEvent event)
{
if (event.getType().equals(StandardContext.AFTER_START_EVENT))
{
// make the context class loader known to the JBossWebMetaData, ws4ee needs it
// to instanciate service endpoint pojos that live in this webapp
metaData.setContextLoader(webLoader.getClassLoader());
Thread currentThread = Thread.currentThread();
ClassLoader currentLoader = currentThread.getContextClassLoader();
try
{
// Create a java:comp/env environment unique for the web application
log.debug("Creating ENC using ClassLoader: " + loader);
ClassLoader parent = loader.getParent();
while (parent != null)
{
log.debug(".." + parent);
parent = parent.getParent();
}
// TODO: The enc should be an input?
currentThread.setContextClassLoader(webLoader.getClassLoader());
metaData.setENCLoader(webLoader.getClassLoader());
InitialContext iniCtx = new InitialContext();
Context envCtx = (Context)iniCtx.lookup("java:comp");
// Add ORB/UserTransaction
ORB orb = null;
try
{
ObjectName ORB_NAME = new ObjectName("jboss:service=CorbaORB");
orb = (ORB)server.getAttribute(ORB_NAME, "ORB");
// Bind the orb
if (orb != null)
{
NonSerializableFactory.rebind(envCtx, "ORB", orb);
log.debug("Bound java:comp/ORB");
}
}
catch (Throwable t)
{
log.debug("Unable to retrieve orb: " + t.toString());
}
// JTA links
envCtx.bind("TransactionSynchronizationRegistry", new LinkRef("java:TransactionSynchronizationRegistry"));
log.debug("Linked java:comp/TransactionSynchronizationRegistry to JNDI name: java:TransactionSynchronizationRegistry");
envCtx.bind("UserTransaction", new LinkRef("UserTransaction"));
log.debug("Linked java:comp/UserTransaction to JNDI name: UserTransaction");
envCtx = envCtx.createSubcontext("env");
injectionContainer.populateEnc(webLoader.getClassLoader());
// TODO: this should be bindings in the metadata
currentThread.setContextClassLoader(webLoader.getClassLoader());
String securityDomain = metaData.getSecurityDomain();
log.debug("linkSecurityDomain");
linkSecurityDomain(securityDomain, envCtx);
}
catch (Throwable t)
{
log.error("ENC setup failed", t);
throw new RuntimeException(t);
}
finally
{
currentThread.setContextClassLoader(currentLoader);
log.debug("injectionContainer enabled and processing beginning");
// we need to do this because the classloader is initialize by the web container and
// the injection container needs the classloader so that it can build up Injectors and ENC populators
injectionContainer.setClassLoader(webLoader.getClassLoader());
injectionContainer.processMetadata();
}
}
}
}
public Loader getWebLoader(DeploymentUnit unit, JBossWebMetaData metaData, ClassLoader loader, URL rl, TomcatInjectionContainer injectionContainer) throws MalformedURLException
{
Loader webLoader;
/*
* If we are using the jboss class loader we need to augment its path to include the WEB-INF/{lib,classes} dirs or
* else scoped class loading does not see the war level overrides. The call to setWarURL adds these paths to the
* deployment UCL.
*/
List<URL> classpath = unit.getAttachment("org.jboss.web.expandedWarClasspath", List.class);
if (classpath == null && unit instanceof VFSDeploymentUnit)
{
VFSDeploymentUnit vfsUnit = (VFSDeploymentUnit)unit;
try
{
VirtualFile classes = vfsUnit.getFile("WEB-INF/classes");
// Tomcat can't handle the vfs urls yet
URL vfsURL = classes.toURL();
String vfsurl = vfsURL.toString();
if (vfsurl.startsWith("vfs"))
vfsURL = new URL(vfsurl.substring(3));
classpath = new ArrayList<URL>();
classpath.add(vfsURL);
}
catch (Exception ignored)
{
}
}
WebCtxLoader jbossLoader = new WebCtxLoader(loader, injectionContainer);
if (classpath != null)
jbossLoader.setClasspath(classpath);
webLoader = jbossLoader;
return webLoader;
}
/**
* Called as part of the undeploy() method template to ask the subclass for perform the web container specific
* undeployment steps.
*/
@Override
protected void performUndeploy(WebApplication warInfo, String warUrl) throws Exception
{
if (warInfo == null)
{
log.debug("performUndeploy, no WebApplication found for URL " + warUrl);
return;
}
log.info("undeploy, ctxPath=" + warInfo.getMetaData().getContextRoot());
JBossWebMetaData metaData = warInfo.getMetaData();
String hostName = null;
// Get any jboss-web/virtual-hosts
List<String> vhostNames = metaData.getVirtualHosts();
// Map the virtual hosts onto the configured hosts
Iterator hostNames = mapVirtualHosts(vhostNames);
if (hostNames.hasNext())
{
hostName = hostNames.next().toString();
}
else
{
hostNames = getDefaultHosts();
if (hostNames.hasNext())
{
hostName = hostNames.next().toString();
}
}
performUndeployInternal(warInfo, hostName, warUrl);
while (hostNames.hasNext())
{
String additionalHostName = hostNames.next().toString();
performUndeployInternal(warInfo, additionalHostName, warUrl);
}
}
protected void performUndeployInternal(WebApplication warInfo, String hostName, String warUrlStr) throws Exception
{
JBossWebMetaData metaData = warInfo.getMetaData();
String ctxPath = metaData.getContextRoot();
// TODO: Need to remove the dependency on MBeanServer
MBeanServer server = MBeanServerLocator.locateJBoss();
// If the server is gone, all apps were stopped already
if (server == null)
return;
ObjectName objectName = new ObjectName(config.getCatalinaDomain() + ":j2eeType=WebModule,name=//" + ((hostName == null) ? "localhost" : hostName) + ctxPath
+ ",J2EEApplication=none,J2EEServer=none");
if (server.isRegistered(objectName))
{
// Contexts should be stopped by the host already
server.invoke(objectName, "destroy", new Object[] {}, new String[] {});
}
}
/**
* Resolve the input virtual host names to the names of the configured Hosts
*
* @param vhostNames Iterator<String> for the jboss-web/virtual-host elements
* @return Iterator<String> of the unique Host names
* @throws Exception
*/
protected synchronized Iterator mapVirtualHosts(List<String> vhostNames) throws Exception
{
if (vhostToHostNames.size() == 0)
{
// Query the configured Host mbeans
String hostQuery = config.getCatalinaDomain() + ":type=Host,*";
ObjectName query = new ObjectName(hostQuery);
Set hosts = server.queryNames(query, null);
Iterator iter = hosts.iterator();
while (iter.hasNext())
{
ObjectName host = (ObjectName)iter.next();
String name = host.getKeyProperty("host");
if (name != null)
{
vhostToHostNames.put(name, name);
String[] aliases = (String[])server.invoke(host, "findAliases", null, null);
int count = aliases != null ? aliases.length : 0;
for (int n = 0; n < count; n++)
{
vhostToHostNames.put(aliases[n], name);
}
}
}
}
// Map the virtual host names to the hosts
HashSet hosts = new HashSet();
if (vhostNames != null)
{
for (String vhost : vhostNames)
{
String host = (String)vhostToHostNames.get(vhost);
if (host == null)
{
log.warn("Failed to map vhost: " + vhost);
// This will cause a new host to be created
host = vhost;
}
hosts.add(host);
}
}
return hosts.iterator();
}
/**
* Find the default hosts for all existing engines
*/
protected synchronized Iterator getDefaultHosts() throws Exception
{
// Map the virtual host names to the hosts
HashSet defaultHosts = new HashSet();
// Query the configured Engine mbeans
String engineQuery = config.getCatalinaDomain() + ":type=Engine,*";
ObjectName query = new ObjectName(engineQuery);
Set engines = server.queryNames(query, null);
Iterator iter = engines.iterator();
while (iter.hasNext())
{
ObjectName engine = (ObjectName)iter.next();
String defaultHost = (String)server.getAttribute(engine, "defaultHost");
if (defaultHost != null)
{
defaultHosts.add(defaultHost);
}
}
return defaultHosts.iterator();
}
/**
* Traverse the parent chain of the context to reach the Catalina Engine
* @param context Context of the web application
* @return
*/
private Engine getCatalinaEngine(org.apache.catalina.Context context)
{
Container parentContainer = context.getParent();
while(parentContainer != null && !(parentContainer instanceof Engine))
parentContainer = parentContainer.getParent();
return (Engine) parentContainer;
}
}