blob: e4d36ae78ac304b349170fe112cbd85973c3dbba [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009, 2017 VMware Inc.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
* and the Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php.
* You may elect to redistribute this code under either of these licenses.
*
* Contributors:
* VMware Inc. - initial contribution
*******************************************************************************/
package org.eclipse.gemini.web.tomcat.internal;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.spi.InitialContextFactory;
import javax.naming.spi.ObjectFactory;
import javax.naming.spi.ObjectFactoryBuilder;
import org.apache.catalina.Authenticator;
import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.Engine;
import org.apache.catalina.Globals;
import org.apache.catalina.Host;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.Server;
import org.apache.catalina.Service;
import org.apache.catalina.Valve;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.startup.Catalina;
import org.apache.catalina.startup.ContextConfig;
import org.apache.catalina.startup.Tomcat;
import org.apache.naming.java.javaURLContextFactory;
import org.apache.tomcat.JarScanner;
import org.apache.tomcat.util.digester.Digester;
import org.apache.tomcat.util.scan.StandardJarScanner;
import org.eclipse.gemini.web.core.spi.ServletContainerException;
import org.eclipse.gemini.web.tomcat.internal.loader.ChainedClassLoader;
import org.eclipse.gemini.web.tomcat.internal.support.BundleFileResolverFactory;
import org.eclipse.gemini.web.tomcat.internal.support.PackageAdminBundleDependencyDeterminer;
import org.eclipse.virgo.util.osgi.ServiceRegistrationTracker;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.osgi.util.tracker.ServiceTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;
public final class OsgiAwareEmbeddedTomcat extends Tomcat {
private static final String USER_DIR = "user.dir";
private static final String ROOT_CONTEXT_PATH = "";
private static final String ROOT_PATH = "/";
static final String USE_NAMING = "useNaming";
static final String TOMCAT_NAMING_ENABLED = "tomcat";
static final String OSGI_NAMING_ENABLED = "osgi";
static final String NAMING_DISABLED = "disabled";
static final String CATALINA_USE_NAMING = "catalina.useNaming";
private static final String JNDI_URLSCHEME = "osgi.jndi.url.scheme";
private static final String JAVA_JNDI_URLSCHEME = "java";
private final static Logger LOGGER = LoggerFactory.getLogger(OsgiAwareEmbeddedTomcat.class);
private final ExtendCatalina catalina = new ExtendCatalina();
private final BundleContext bundleContext;
private Path configDir;
private String defaultContextXml;
private String defaultWeb;
private String oldCatalinaBaseDir;
private String oldCatalinaHomeDir;
/**
* Custom mappings of login methods to authenticators
*/
private volatile HashMap<String, Authenticator> authenticators;
private final Object monitor = new Object();
private final ServiceRegistrationTracker tracker = new ServiceRegistrationTracker();
private final DelegatingJarScannerCustomizer jarScannerCustomizer;
private final JarScanner bundleDependenciesJarScanner;
private final JarScanner defaultJarScanner;
private String hostConfigDir;
OsgiAwareEmbeddedTomcat(BundleContext context, ServiceTracker<?, ?> urlConverterTracker) {
this.bundleContext = context;
this.bundleDependenciesJarScanner = new BundleDependenciesJarScanner(new PackageAdminBundleDependencyDeterminer(),
BundleFileResolverFactory.createBundleFileResolver(), context, urlConverterTracker);
this.defaultJarScanner = new StandardJarScanner();
this.jarScannerCustomizer = new DelegatingJarScannerCustomizer(context);
}
/**
* Start the server.
*
* @throws LifecycleException
*/
@Override
public void start() throws LifecycleException {
this.jarScannerCustomizer.open();
getServer();
this.server.start();
}
@Override
public void stop() throws LifecycleException {
super.stop();
this.jarScannerCustomizer.close();
}
public void setServer(Server server) {
this.server = server;
}
@Override
public Service getService() {
Server server = getServer();
Service[] findServices = server.findServices();
if (findServices != null && findServices.length > 0) {
return findServices[0];
}
throw new IllegalStateException("Unable to locate Service.");
}
@Override
public Host getHost() {
return findHost();
}
@Override
public Engine getEngine() {
return findEngine();
}
private Engine findEngine() {
Server server = getServer();
Service[] findServices = server.findServices();
for (Service service : findServices) {
Engine container = service.getContainer();
if (container != null) {
return container;
}
}
throw new IllegalStateException("Unable to locate Engine.");
}
private Host findHost() {
Engine engine = findEngine();
Container[] children = engine.findChildren();
for (Container container : children) {
if (container instanceof Host) {
return (Host) container;
}
}
throw new IllegalStateException("Unable to locate Host.");
}
@Override
public void init() throws LifecycleException {
getServer();
initNaming();
this.server.init();
}
private void initNaming() {
String useNaming = this.bundleContext.getProperty(USE_NAMING);
if (useNaming == null) {
useNaming = System.getProperty(USE_NAMING);
}
if (useNaming == null) {
useNaming = TOMCAT_NAMING_ENABLED;
}
if (NAMING_DISABLED.equals(useNaming)) {
System.setProperty(CATALINA_USE_NAMING, Boolean.FALSE.toString());
} else {
enableNaming(useNaming);
}
}
private void enableNaming(String useNaming) {
super.enableNaming();
if (OSGI_NAMING_ENABLED.equals(useNaming)) {
registerInitialContextFactory();
registerJavaURLContextFactory();
registerObjectFactoryBuilder();
}
}
@Override
public void destroy() throws LifecycleException {
super.destroy();
this.tracker.unregisterAll();
if (this.oldCatalinaBaseDir != null) {
System.setProperty(Globals.CATALINA_BASE_PROP, this.oldCatalinaBaseDir);
}
if (this.oldCatalinaHomeDir != null) {
System.setProperty(Globals.CATALINA_HOME_PROP, this.oldCatalinaHomeDir);
}
}
@Override
public Context addWebapp(String path, String docBase) {
return addWebapp(path, docBase, null);
}
Context addWebapp(String path, String docBase, Bundle bundle) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Creating context [" + path + "] with docBase [" + docBase + "].");
}
StandardContext context = new ExtendedStandardContext(bundle);
ContextConfig config = new ExtendedContextConfig();
if (this.configDir == null) {
// Allocate the tomcat's configuration directory
this.configDir = TomcatConfigLocator.resolveConfigDir(this.bundleContext);
}
if (this.defaultWeb == null) {
// Allocate the default web.xml
this.defaultWeb = WebappConfigLocator.resolveDefaultWebXml(this.configDir);
}
config.setDefaultWebXml(this.defaultWeb);
// Allocate default context.xml
if (this.defaultContextXml == null) {
this.defaultContextXml = WebappConfigLocator.resolveDefaultContextXml(this.configDir);
}
context.setDefaultContextXml(this.defaultContextXml);
Host host = getHost();
if (this.hostConfigDir == null) {
this.hostConfigDir = TomcatConfigLocator.resolveHostConfigDir(this.configDir, host);
}
if (this.hostConfigDir != null) {
host.setXmlBase(this.hostConfigDir);
host.getConfigBaseFile();
}
// Allocate the web application's configuration directory
Path configLocation = WebappConfigLocator.resolveWebappConfigDir(this.configDir, host);
// If web application's context.xml is existing, set it to the StandardContext
try {
context.setConfigFile(WebappConfigLocator.resolveWebappContextXml(path, docBase, configLocation, bundle));
} catch (MalformedURLException e) {
throw new ServletContainerException("Cannot resolve web application's context.xml [" + docBase + "].", e);
}
context.setDocBase(docBase);
context.setPath(path.equals(ROOT_PATH) ? ROOT_CONTEXT_PATH : path);
context.setName(context.getPath());
context.setJarScanner(getJarScanner(bundle));
context.setParent(host);
config.setCustomAuthenticators(this.authenticators);
((Lifecycle) context).addLifecycleListener(config);
return context;
}
void configure(InputStream configuration) {
Digester digester = this.catalina.createStartDigester();
digester.push(this);
ClassLoader[] loaders = new ClassLoader[] { Catalina.class.getClassLoader(), getClass().getClassLoader() };
ChainedClassLoader chainedClassLoader = ChainedClassLoader.create(loaders);
chainedClassLoader.setBundle(this.bundleContext.getBundle());
digester.setClassLoader(chainedClassLoader);
try {
digester.parse(configuration);
} catch (IOException e) {
throw new ServletContainerException("Error reading Tomcat configuration file.", e);
} catch (SAXException e) {
throw new ServletContainerException("Error parsing Tomcat XML configuration.", e);
}
initBaseDir();
// Allocate the tomcat's configuration directory
this.configDir = TomcatConfigLocator.resolveConfigDir(this.bundleContext);
// Allocate default context.xml
this.defaultContextXml = WebappConfigLocator.resolveDefaultContextXml(this.configDir);
// Allocate the default web.xml
this.defaultWeb = WebappConfigLocator.resolveDefaultWebXml(this.configDir);
Host host = getHost();
this.hostConfigDir = TomcatConfigLocator.resolveHostConfigDir(this.configDir, host);
if (this.hostConfigDir != null) {
host.setXmlBase(this.hostConfigDir);
host.getConfigBaseFile();
}
}
@Override
protected void initBaseDir() {
String catalinaHome = System.getProperty(Globals.CATALINA_HOME_PROP);
if (this.basedir == null) {
this.basedir = System.getProperty(Globals.CATALINA_BASE_PROP);
}
if (this.basedir == null) {
this.basedir = catalinaHome;
}
if (this.basedir == null) {
// Create a temp dir.
this.basedir = System.getProperty(USER_DIR) + "/tomcat." + this.port;
}
Path baseFile = Paths.get(this.basedir);
try {
Files.createDirectories(baseFile);
} catch (IOException e1) {
throw new IllegalStateException("Cannot create directories for [" + baseFile + "].", e1);
}
try {
this.basedir = baseFile.toRealPath().toString();
} catch (IOException e) {
this.basedir = baseFile.toAbsolutePath().toString();
}
this.server.setCatalinaBase(new File(this.basedir));
this.oldCatalinaBaseDir = System.setProperty(Globals.CATALINA_BASE_PROP, this.basedir);
if (catalinaHome == null) {
this.server.setCatalinaHome(new File(this.basedir));
} else {
Path homeFile = Paths.get(catalinaHome);
try {
Files.createDirectories(homeFile);
} catch (IOException e1) {
throw new IllegalStateException("Cannot create directories for [" + homeFile + "].", e1);
}
try {
this.server.setCatalinaHome(homeFile.toRealPath().toFile());
} catch (IOException e) {
this.server.setCatalinaHome(homeFile.toAbsolutePath().toFile());
}
}
this.oldCatalinaHomeDir = System.setProperty(Globals.CATALINA_HOME_PROP, this.basedir);
}
/**
* Maps the specified login method to the specified authenticator, allowing the mappings in
* org/apache/catalina/startup/Authenticators.properties to be overridden.
*
* @param authenticator Authenticator to handle authentication for the specified login method
* @param loginMethod Login method that maps to the specified authenticator
*
* @throws IllegalArgumentException if the specified authenticator does not implement the org.apache.catalina.Valve
* interface
*/
void addAuthenticator(Authenticator authenticator, String loginMethod) {
if (!(authenticator instanceof Valve)) {
throw new IllegalArgumentException("Specified Authenticator is not a Valve");
}
if (this.authenticators == null) {
synchronized (this.monitor) {
if (this.authenticators == null) {
this.authenticators = new HashMap<>();
}
}
}
this.authenticators.put(loginMethod, authenticator);
}
/**
* Overrides {@link Catalina} to provide public access to {@link Catalina#createStartDigester}.
*
*
*/
private static class ExtendCatalina extends Catalina {
@Override
public Digester createStartDigester() {
return super.createStartDigester();
}
}
/**
* Override {@link ContextConfig}. This changes the {@link ClassLoader} used to load the web-embed.xml to the
* <code>ClassLoader</code> of this bundle.
*
*
*/
private static class ExtendedContextConfig extends ContextConfig {
}
/**
* Registers the <code>ObjectFactoryBuilder</code> implementation that is responsible for loading Tomcat's Object
* Factories.
*/
private void registerObjectFactoryBuilder() {
ServiceRegistration<ObjectFactoryBuilder> serviceRegistration = this.bundleContext.registerService(ObjectFactoryBuilder.class,
new ObjectFactoryBuilder() {
@Override
public ObjectFactory createObjectFactory(Object obj, Hashtable<?, ?> environment) throws NamingException {
if (obj instanceof Reference) {
final Reference reference = (Reference) obj;
final String factory = reference.getFactoryClassName();
if (factory != null) {
try {
Class<?> clazz = getClass().getClassLoader().loadClass(factory);
return (ObjectFactory) clazz.newInstance();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Error while trying to create object factory [" + factory + "]", e);
}
}
}
}
return null;
}
}, null);
this.tracker.track(serviceRegistration);
}
/**
* Registers <code>org.apache.naming.java.javaURLContextFactory</code> as URL Context Factory for 'java' URL scheme.
* In the traditional way this factory is specified via <code>java.naming.factory.initial</code> system property.
*/
private void registerJavaURLContextFactory() {
Dictionary<String, String> serviceProperties = new Hashtable<>();
serviceProperties.put(JNDI_URLSCHEME, JAVA_JNDI_URLSCHEME);
ServiceRegistration<ObjectFactory> serviceRegistration = this.bundleContext.registerService(ObjectFactory.class, new javaURLContextFactory(),
serviceProperties);
this.tracker.track(serviceRegistration);
}
/**
* Registers the <code>InitialContextFactory</code> implementation that is responsible for loading
* <code>org.apache.naming.java.javaURLContextFactory</code>. In the traditional way this factory is specified via
* <code>java.naming.factory.initial</code> system property.
*/
private void registerInitialContextFactory() {
ServiceRegistration<?> serviceRegistration = this.bundleContext.registerService(new String[] { InitialContextFactory.class.getName(),
javaURLContextFactory.class.getName() }, new javaURLContextFactory(), null);
this.tracker.track(serviceRegistration);
}
private JarScanner getJarScanner(Bundle bundle) {
JarScanner[] jarScanners = new JarScanner[] { this.bundleDependenciesJarScanner, this.defaultJarScanner };
JarScanner[] chainExtensions = this.jarScannerCustomizer.extendJarScannerChain(bundle);
JarScanner[] finalJarScanners = null;
if (chainExtensions != null && chainExtensions.length > 0) {
finalJarScanners = new JarScanner[jarScanners.length + chainExtensions.length];
System.arraycopy(jarScanners, 0, finalJarScanners, 0, jarScanners.length);
System.arraycopy(chainExtensions, 0, finalJarScanners, jarScanners.length, chainExtensions.length);
} else {
finalJarScanners = jarScanners;
}
return new ChainingJarScanner(finalJarScanners);
}
}