blob: 8a9f2a78dd3c2feaf2170131284a0c8ada346756 [file] [log] [blame]
//
// ========================================================================
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// 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
//
// 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.
// ========================================================================
//
package org.eclipse.jetty.overlays;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Timer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import org.eclipse.jetty.deploy.App;
import org.eclipse.jetty.deploy.AppProvider;
import org.eclipse.jetty.deploy.ConfigurationManager;
import org.eclipse.jetty.deploy.DeploymentManager;
import org.eclipse.jetty.jndi.java.javaRootURLContext;
import org.eclipse.jetty.jndi.local.localContextRoot;
import org.eclipse.jetty.server.ResourceCache;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.servlet.Holder;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.Scanner;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.JarResource;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceCollection;
import org.eclipse.jetty.webapp.JettyWebXmlConfiguration;
import org.eclipse.jetty.webapp.WebAppClassLoader;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.xml.XmlConfiguration;
import org.xml.sax.SAXException;
/**
* Overlayed AppProvider
* <p>
* This {@link AppProvider} implementation can deploy either {@link WebAppContext}s or plain
* {@link ContextHandler}s that are assembled from a series of overlays:
* <dl>
* <dt>webapp</dt><dd>The webapp overlay is a WAR file or docroot directory. The intent is that
* the WAR should be deployed to this AppProvider unchanged from how it was delivered. All configuration
* and extension should be able to be done in an overlay.</dd>
* <dt>template</dt><dd>A template overlay is applied to a WAR file to configure it for all instances of
* the webapp to be deployed in the server(s)</dd>
* <dt>node</dt><dd>A node overlay is applied to a template to configure it all instances of the template
* with node specific information (eg IP address, DB servers etc.).</dd>
* <dt>instance</dt><dd>An instance overlay is applied to a node and/or template to configure it
* for a specific instance of the template (eg per tenant configuration).</dd>
* </dl>
* <p>
* Each overlays may provide the following files and subdirectories:<dl>
* <dt>WEB-INF/lib-overlay</dt>
* <dd>The lib-overlay directory can contain jars that are applied to a {@link URLClassLoader} that is
* available before any overlay.xml files are executed, so that classes from these jars may be used by the
* overlay.xml.</dd>
*
* <dt>WEB-INF/overlay.xml</dt>
* <dd>This {@link XmlConfiguration} formatted file must exist in the WEB-INF directory of an overlay and is
* used to configure a {@link ContextHandler} or {@link WebAppContext}. The overlay.xml from the template
* overlay can be used to instantiate the ContextHandler instance, so a derived class maybe used.</dd>
*
* <dt>WEB-INF/template.xml</dt>
* <dd>This {@link XmlConfiguration} formatted file if it exists in a template or node overlay, is applied to a shared instance of {@link TemplateContext}.
* Any ID's created in a template are available as ID's in overlay.xml for an instance.</dd>
*
* <dt>WEB-INF/webdefault.xml</dt>
* <dd>If present in an overlay, then the most specific version is passed to
* {@link WebAppContext#setDefaultsDescriptor(String)}. Typically this is set in the template overlay.</dd>
*
* <dt>WEB-INF/web-overlay.xml</dt>
* <dd>The web-overlay.xml file of an overlay is applied to a web application as
* with {@link WebAppContext#addOverrideDescriptor(String)}. This allows incremental changes to web.xml without
* totally replacing it (see webapp). Typically this is used to set init parameters.</dd>
*
* <dt>.</dt>
* <dd>This root directory contains static content that overlays the static content of the webapp
* or earlier overlays. Using this directory, files like index.html or logo.png can be added or replaced. It can
* also be used to replace files within WEB-INF including web.xml classes and libs.</dd>
* </dl>
* <p>
* Any init parameters set on the context, filters or servlets may have parameterized values, with the parameters
* including:
* <dl>
* <dt>${overlays.dir}</dt>
* <dd>the root overlay scan directory as a canonical file name.</dd>
* <dt>${overlay.webapp}</dt>
* <dd>the webapp name, same as {@link Webapp#getName()}.</dd>
* <dt>${overlay.template}</dt>
* <dd>the template name, as {@link Template#getName()}.</dd>
* <dt>${overlay.template.name}</dt>
* <dd>the template classifier, as {@link Template#getTemplateName()}.</dd>
* <dt>${overlay.template.classifier}</dt>
* <dd>the template classifier, as {@link Template#getClassifier()()}.</dd>
* <dt>${overlay.node}</dt>
* <dd>the node name, as {@link Node#getName()}.</dd>
* <dt>${overlay.instance}</dt>
* <dd>the instance name, {@link Instance#getName()}.</dd>
* <dt>${overlay.instance.classifier}</dt>
* <dd>the instance name, {@link Instance#getClassifier()()}.</dd>
* <dt>${*}</dt>
* <dd>Any properties obtained via {@link #getConfigurationManager()}.{@link ConfigurationManager#getProperties()}</dd>
* <dd></dd>
* </dl>
* <p>
* The OverlayedAppProvider will scan the "webapps", "templates", "nodes" and "instances" subdirectories of
* the directory configured with {@link #setScanDir(File)}. New webapps and overlays and modified files within
* the overlays will trigger hot deployment, redeployment or undeployment. The scan for modified files is
* restricted to only top level files (eg overlay.xml) and the files matching WEB-INF/*.xml WEB-INF/lib/*
* and WEB-INF/classes/*. The webapps/overlays may be directory structures or war/jar archives.
* <p>
* The filenames of the templates and instances are used to match them together and with a webapplication.
* A webapp may be named anyway, but it is good practise to include a version number (eg webapps/foo-1.2.3.war
* or webapps/foo-1.2.3/). A template for that webapplication must have a name that includes the template name
* and the war name separated by '=' (eg templates/myFoo=foo-1.2.3.jar or templates/myFoo=foo-1.2.3/).
* An instance overlay is named with the template name and an arbitrary instance name separated by '='
* (eg instances/myFoo=instance1.jar instances/myFoo=instance2/ etc.).
* <p>
* If a template name does not include a webapp name, then the template is created as a ContextHandler
* instead of a WebAppContext (with the exact type being determined by overlay.xml).
*/
public class OverlayedAppProvider extends AbstractLifeCycle implements AppProvider
{
private final static Logger __log=org.eclipse.jetty.util.log.Log.getLogger("OverlayedAppProvider");
/**
* Property set for overlay.xml and template.xml files that gives the root overlay scan directory as a canonical file name.
*/
public final static String OVERLAYS_DIR="overlays.dir";
/**
* Property set for overlay.xml and template.xml files that gives the current webapp name, as {@link Webapp#getName()}.
*/
public final static String OVERLAY_WEBAPP="overlay.webapp";
/**
* Property set for overlay.xml and template.xml files that gives the current template full name, as {@link Template#getName()}.
*/
public final static String OVERLAY_TEMPLATE="overlay.template";
/**
* Property set for overlay.xml and template.xml files that gives the current template name, as {@link Template#getTemplateName()}.
*/
public final static String OVERLAY_TEMPLATE_NAME="overlay.template.name";
/**
* Property set for overlay.xml and template.xml files that gives the current template classifier, as {@link Template#getClassifier()}.
*/
public final static String OVERLAY_TEMPLATE_CLASSIFIER="overlay.template.classifier";
/**
* Property set for overlay.xml and template.xml files that gives the current node name, as {@link Node#getName()}.
*/
public final static String OVERLAY_NODE="overlay.node";
/**
* Property set for overlay.xml and template.xml files that gives the current instance name, {@link Instance#getName()}.
*/
public final static String OVERLAY_INSTANCE="overlay.instance";
/**
* Property set for overlay.xml and template.xml files that gives the current instance clasifier, {@link Instance#getClassifier()}.
*/
public final static String OVERLAY_INSTANCE_CLASSIFIER="overlay.instance.classifier";
public final static String WEBAPPS="webapps";
public final static String TEMPLATES="templates";
public final static String NODES="nodes";
public final static String INSTANCES="instances";
public final static String LIB="WEB-INF/lib-overlay";
public final static String WEBAPP=".";
public final static String OVERLAY_XML="WEB-INF/overlay.xml";
public final static String TEMPLATE_XML="WEB-INF/template.xml";
public final static String WEB_DEFAULT_XML="WEB-INF/web-default.xml";
public final static String WEB_FRAGMENT_XML="WEB-INF/web-overlay.xml";
enum Monitor { WEBAPPS,TEMPLATES,NODES,INSTANCES} ;
public final static List<Pattern> __scanPatterns = new ArrayList<Pattern>();
static
{
List<String> regexes = new ArrayList<String>();
for (String s:new String[] {".war",".jar","/WEB-INF/syslib/[^/]*","/WEB-INF/lib/[^/]*","/WEB-INF/classes/[^/]*","/WEB-INF/[^/]*\\.xml",})
{
regexes.add(WEBAPPS+"/[^/]*"+s);
regexes.add(TEMPLATES+"/[^/]*"+s);
regexes.add(NODES+"/[^/]*"+s);
regexes.add(INSTANCES+"/[^/]*"+s);
}
for (String s: regexes)
__scanPatterns.add(Pattern.compile(s,Pattern.CASE_INSENSITIVE));
};
private String _nodeName;
private File _scanDir;
private File _tmpDir;
private String _scanDirURI;
private long _loading;
private Node _node;
private final Map<String,Webapp> _webapps = new HashMap<String,Webapp>();
private final Map<String,Template> _templates = new HashMap<String,Template>();
private final Map<String,Instance> _instances = new HashMap<String,Instance>();
private final Map<String,OverlayedApp> _deployed = new HashMap<String,OverlayedApp>();
private final Map<String,TemplateContext> _shared = new HashMap<String, TemplateContext>();
private boolean _copydir=false;
private DeploymentManager _deploymentManager;
private ConfigurationManager _configurationManager;
private String _serverID="Server";
private final Set<Layer> _removedLayers = new HashSet<Layer>();
private Timer _sessionScavenger = new Timer();
private final Scanner _scanner = new Scanner();
private final Scanner.BulkListener _listener = new Scanner.BulkListener()
{
public void filesChanged(List<String> filenames) throws Exception
{
__log.debug("Changed {}",filenames);
Set<String> changes = new HashSet<String>();
for (String filename:filenames)
{
File file=new File(filename);
if (file.getName().startsWith(".") || file.getName().endsWith(".swp"))
continue;
String relname=file.toURI().getPath().substring(_scanDirURI.length());
File rel = new File(relname);
String dir=null;
String name=null;
String parent=rel.getParent();
while (parent!=null)
{
name=rel.getName();
dir=parent;
rel=rel.getParentFile();
parent=rel.getParent();
}
String uri=dir+"/"+name;
for (Pattern p : __scanPatterns)
{
if (p.matcher(relname).matches())
{
__log.debug("{} == {}",relname,p.pattern());
changes.add(uri);
}
else
__log.debug("{} != {}",relname,p.pattern());
}
}
if (changes.size()>0)
OverlayedAppProvider.this.updateLayers(changes);
}
};
/* ------------------------------------------------------------ */
public OverlayedAppProvider()
{
try
{
_nodeName=InetAddress.getLocalHost().getHostName();
}
catch(UnknownHostException e)
{
__log.debug(e);
_nodeName="unknown";
}
}
/* ------------------------------------------------------------ */
public void setDeploymentManager(DeploymentManager deploymentManager)
{
_deploymentManager=deploymentManager;
}
/* ------------------------------------------------------------ */
public DeploymentManager getDeploymentManager()
{
return _deploymentManager;
}
/* ------------------------------------------------------------ */
public ConfigurationManager getConfigurationManager()
{
return _configurationManager;
}
/* ------------------------------------------------------------ */
/** Set the configurationManager.
* @param configurationManager the configurationManager to set
*/
public void setConfigurationManager(ConfigurationManager configurationManager)
{
_configurationManager = configurationManager;
}
/* ------------------------------------------------------------ */
/**
* @return The name in {@link XmlConfiguration#getIdMap()} of the {@link Server} instance. Default "Server".
*/
public String getServerID()
{
return _serverID;
}
/* ------------------------------------------------------------ */
/**
* @param serverID The name in {@link XmlConfiguration#getIdMap()} of the {@link Server} instance.
*/
public void setServerID(String serverID)
{
_serverID = serverID;
}
/**
* Create Context Handler.
* <p>
* Callback from the deployment manager to create a context handler instance.
* @see org.eclipse.jetty.deploy.AppProvider#createContextHandler(org.eclipse.jetty.deploy.App)
*/
public synchronized ContextHandler createContextHandler(App app) throws Exception
{
final OverlayedApp overlayed = (OverlayedApp)app;
final String origin = overlayed.getOriginId();
final Instance instance = overlayed.getInstance();
final Template template = instance.getTemplate();
final Webapp webapp = template.getWebapp();
final Node node = _node;
// remember the original loader
ClassLoader orig_loader = Thread.currentThread().getContextClassLoader();
try
{
// Look for existing shared resources
String key=(node==null?"":node.getLoadedKey())+template.getLoadedKey()+(webapp==null?"":webapp.getLoadedKey());
instance.setSharedKey(key);
TemplateContext shared=_shared.get(key);
// Create shared resourced
if (shared==null)
shared=createTemplateContext(key,webapp,template,node,orig_loader);
// Build the instance lib loader
ClassLoader shared_loader = shared.getWebappLoader()!=null?shared.getWebappLoader():(shared.getLibLoader()!=null?shared.getLibLoader():orig_loader);
ClassLoader loader = shared_loader;
Resource instance_lib = instance.getResource(LIB);
if (instance_lib.exists())
{
List<URL> libs = new ArrayList<URL>();
for (String jar :instance_lib.list())
{
if (!jar.toLowerCase(Locale.ENGLISH).endsWith(".jar"))
continue;
libs.add(instance_lib.addPath(jar).getURL());
}
__log.debug("{}: libs={}",origin,libs);
loader = URLClassLoader.newInstance(libs.toArray(new URL[]{}),loader);
}
// set the thread loader
Thread.currentThread().setContextClassLoader(loader);
// Create properties to be shared by overlay.xmls
Map<String,Object> idMap = new HashMap<String,Object>();
idMap.putAll(shared.getIdMap());
idMap.put(_serverID,getDeploymentManager().getServer());
// Create the instance context for the template
ContextHandler context=null;
Resource template_context_xml = template.getResource(OVERLAY_XML);
if (template_context_xml.exists())
{
__log.debug("{}: overlay.xml={}",origin,template_context_xml);
XmlConfiguration xmlc = newXmlConfiguration(template_context_xml.getURL(),idMap,template,instance);
context=(ContextHandler)xmlc.configure();
idMap=xmlc.getIdMap();
}
else if (webapp==null)
// If there is no webapp, this is a plain context
context=new ContextHandler();
else
// It is a webapp context
context=new WebAppContext();
// Set the resource base
final Resource instance_webapp = instance.getResource(WEBAPP);
if (instance_webapp.exists())
{
context.setBaseResource(new ResourceCollection(instance_webapp,shared.getBaseResource()));
// Create the resource cache
ResourceCache cache = new ResourceCache(shared.getResourceCache(),instance_webapp,context.getMimeTypes(),false,false);
context.setAttribute(ResourceCache.class.getCanonicalName(),cache);
}
else
{
context.setBaseResource(shared.getBaseResource());
context.setAttribute(ResourceCache.class.getCanonicalName(),shared.getResourceCache());
}
__log.debug("{}: baseResource={}",origin,context.getResourceBase());
// Set the shared session scavenger timer
context.setAttribute("org.eclipse.jetty.server.session.timer", _sessionScavenger);
// Apply any node or instance overlay.xml
for (Resource context_xml : getLayeredResources(OVERLAY_XML,node,instance))
{
__log.debug("{}: overlay.xml={}",origin,context_xml);
XmlConfiguration xmlc = newXmlConfiguration(context_xml.getURL(),idMap,template,instance);
xmlc.getIdMap().put("Cache",context.getAttribute(ResourceCache.class.getCanonicalName()));
xmlc.configure(context);
idMap=xmlc.getIdMap();
}
// Is it a webapp?
if (context instanceof WebAppContext)
{
final WebAppContext webappcontext = (WebAppContext)context;
if (Arrays.asList(((WebAppContext)context).getServerClasses()).toString().equals(Arrays.asList(WebAppContext.__dftServerClasses).toString()))
{
__log.debug("clear server classes");
webappcontext.setServerClasses(null);
}
// set classloader
webappcontext.setCopyWebDir(false);
webappcontext.setCopyWebInf(false);
webappcontext.setExtractWAR(false);
if (instance_webapp.exists())
{
final Resource classes=instance_webapp.addPath("WEB-INF/classes");
final Resource lib=instance_webapp.addPath("WEB-INF/lib");
if (classes.exists()||lib.exists())
{
final AtomicBoolean locked =new AtomicBoolean(false);
WebAppClassLoader webapp_loader=new WebAppClassLoader(loader,webappcontext)
{
@Override
public void addClassPath(Resource resource) throws IOException
{
if (!locked.get())
super.addClassPath(resource);
}
@Override
public void addClassPath(String classPath) throws IOException
{
if (!locked.get())
super.addClassPath(classPath);
}
@Override
public void addJars(Resource lib)
{
if (!locked.get())
super.addJars(lib);
}
};
if (classes.exists())
webapp_loader.addClassPath(classes);
if (lib.exists())
webapp_loader.addJars(lib);
locked.set(true);
loader=webapp_loader;
}
}
// Make sure loader is unique for JNDI
if (loader==shared_loader)
loader = new URLClassLoader(new URL[]{},shared_loader);
// add default descriptor
List<Resource> webdefaults=getLayeredResources(WEB_DEFAULT_XML,instance,node,template);
if (webdefaults.size()>0)
{
Resource webdefault = webdefaults.get(0);
__log.debug("{}: defaultweb={}",origin,webdefault);
webappcontext.setDefaultsDescriptor(webdefault.toString());
}
// add overlay descriptors
for (Resource override : getLayeredResources(WEB_FRAGMENT_XML,template,node,instance))
{
__log.debug("{}: web override={}",origin,override);
webappcontext.addOverrideDescriptor(override.toString());
}
}
context.setClassLoader(loader);
__log.debug("{}: baseResource={}",origin,context.getBaseResource());
Resource jetty_web_xml = context.getResource("/WEB-INF/"+JettyWebXmlConfiguration.JETTY_WEB_XML);
if (jetty_web_xml!=null && jetty_web_xml.exists())
context.setAttribute(JettyWebXmlConfiguration.XML_CONFIGURATION,newXmlConfiguration(jetty_web_xml.getURL(),idMap,template,instance));
// Add listener to expand parameters from descriptors before other listeners execute
Map<String,String> params = new HashMap<String,String>();
populateParameters(params,template,instance);
context.addEventListener(new ParameterExpander(params,context));
System.err.println("created:\n"+context.dump());
return context;
}
finally
{
Thread.currentThread().setContextClassLoader(orig_loader);
}
}
/* ------------------------------------------------------------ */
private XmlConfiguration newXmlConfiguration(URL url, Map<String, Object> idMap, Template template, Instance instance) throws SAXException, IOException
{
XmlConfiguration xmlc = new XmlConfiguration(url);
populateParameters(xmlc.getProperties(),template,instance);
xmlc.getIdMap().putAll(idMap);
return xmlc;
}
/* ------------------------------------------------------------ */
private void populateParameters(Map<String,String> params,Template template, Instance instance)
{
try
{
params.put(OVERLAYS_DIR,_scanDir.getCanonicalPath());
if (template!=null)
{
params.put(OVERLAY_TEMPLATE,template.getName());
params.put(OVERLAY_TEMPLATE_NAME,template.getTemplateName());
params.put(OVERLAY_TEMPLATE_CLASSIFIER,template.getClassifier());
params.put(OVERLAY_WEBAPP,template.getWebapp()==null?null:template.getWebapp().getName());
}
if (_node!=null)
params.put(OVERLAY_NODE,_node.getName());
if (instance!=null)
{
params.put(OVERLAY_INSTANCE,instance.getName());
params.put(OVERLAY_INSTANCE_CLASSIFIER,instance.getClassifier());
}
if (getConfigurationManager()!=null)
params.putAll(getConfigurationManager().getProperties());
}
catch(Exception e)
{
throw new RuntimeException(e);
}
}
/* ------------------------------------------------------------ */
private TemplateContext createTemplateContext(final String key, Webapp webapp, Template template, Node node, ClassLoader parent) throws Exception
{
__log.info("created {}",key);
// look for libs
// If we have libs directories, create classloader and make it available to
// the XMLconfiguration
List<URL> libs = new ArrayList<URL>();
for (Resource lib : getLayeredResources(LIB,node,template))
{
for (String jar :lib.list())
{
if (!jar.toLowerCase(Locale.ENGLISH).endsWith(".jar"))
continue;
libs.add(lib.addPath(jar).getURL());
}
}
final ClassLoader libLoader;
if (libs.size()>0)
{
__log.debug("{}: libs={}",key,libs);
libLoader=new URLClassLoader(libs.toArray(new URL[]{}),parent)
{
public String toString() {return "libLoader@"+Long.toHexString(hashCode())+"-lib-"+key;}
};
}
else
libLoader=parent;
Thread.currentThread().setContextClassLoader(libLoader);
// Make the shared resourceBase
List<Resource> bases = new ArrayList<Resource>();
for (Resource wa : getLayers(node,template))
bases.add(wa);
if (webapp!=null)
bases.add(webapp.getBaseResource());
Resource baseResource = bases.size()==1?bases.get(0):new ResourceCollection(bases.toArray(new Resource[bases.size()]));
__log.debug("{}: baseResource={}",key,baseResource);
// Make the shared context
TemplateContext shared = new TemplateContext(key,getDeploymentManager().getServer(),baseResource,libLoader);
_shared.put(key,shared);
// Create properties to be shared by overlay.xmls
Map<String,Object> idMap = new HashMap<String,Object>();
idMap.put(_serverID,getDeploymentManager().getServer());
// Create the shared context for the template
// This instance will never be start, but is used to capture the
// shared results of running the template and node overlay.xml files.
// If there is a template overlay.xml, give it the chance to create the ContextHandler instance
// otherwise create an instance ourselves
for (Resource template_xml : getLayeredResources(TEMPLATE_XML,template,node))
{
__log.debug("{}: template.xml={}",key,template_xml);
XmlConfiguration xmlc = newXmlConfiguration(template_xml.getURL(),idMap,template,null);
xmlc.getIdMap().putAll(idMap);
xmlc.configure(shared);
idMap=xmlc.getIdMap();
}
shared.setIdMap(idMap);
shared.start();
return shared;
}
/* ------------------------------------------------------------ */
/**
* @return The node name (defaults to hostname)
*/
public String getNodeName()
{
return _nodeName;
}
/* ------------------------------------------------------------ */
/**
* @param nodeName Set the node name
*/
public void setNodeName(String nodeName)
{
_nodeName = nodeName;
}
/* ------------------------------------------------------------ */
/** Get the scanDir.
* @return the scanDir
*/
public File getScanDir()
{
return _scanDir;
}
/* ------------------------------------------------------------ */
/** Set the scanDir.
* @param scanDir the scanDir to set
*/
public void setScanDir(File scanDir)
{
_scanDir = scanDir;
}
/* ------------------------------------------------------------ */
/** Set the temporary directory.
* @param tmpDir the directory for temporary files. If null, then getScanDir()+"/tmp" is used if it exists, else the system default is used.
*/
public void setTmpDir(File tmpDir)
{
_tmpDir=tmpDir;
}
/* ------------------------------------------------------------ */
/** Get the temporary directory.
* return the tmpDir. If null, then getScanDir()+"/tmp" is used if it exists, else the system default is used.
*/
public File getTmpDir()
{
return _tmpDir;
}
/* ------------------------------------------------------------ */
/**
* @return The scan interval
* @see org.eclipse.jetty.util.Scanner#getScanInterval()
*/
public int getScanInterval()
{
return _scanner.getScanInterval();
}
/* ------------------------------------------------------------ */
/**
* @param scanInterval The scan interval
* @see org.eclipse.jetty.util.Scanner#setScanInterval(int)
*/
public void setScanInterval(int scanInterval)
{
_scanner.setScanInterval(scanInterval);
}
/* ------------------------------------------------------------ */
/**
* @see org.eclipse.jetty.util.Scanner#scan()
*/
public void scan()
{
_scanner.scan();
}
/* ------------------------------------------------------------ */
/**
* @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStart()
*/
@Override
protected void doStart() throws Exception
{
__log.info("Node={} Scan=",_nodeName,_scanDir);
if (_scanDir==null || !_scanDir.exists())
throw new IllegalStateException("!scandir");
_scanDirURI=_scanDir.toURI().getPath();
_scanner.setScanDepth(6); // enough for templates/name/webapps/WEB-INF/lib/foo.jar
List<File> dirs = Arrays.asList(new File[]
{
new File(_scanDir,WEBAPPS),
new File(_scanDir,TEMPLATES),
new File(_scanDir,NODES),
new File(_scanDir,INSTANCES)
});
for (File file : dirs)
{
if (!file.exists() && !file.isDirectory())
__log.warn("No directory: "+file.getAbsolutePath());
}
_scanner.setScanDirs(dirs);
_scanner.addListener(_listener);
_scanner.start();
super.doStart();
}
/* ------------------------------------------------------------ */
/**
* @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStop()
*/
@Override
protected void doStop() throws Exception
{
_scanner.removeListener(_listener);
_scanner.stop();
if (_deploymentManager.isRunning())
{
for (App app: _deployed.values())
_deploymentManager.removeApp(app);
}
_deployed.clear();
for (Layer layer : _webapps.values())
layer.release();
_webapps.clear();
for (Layer layer : _templates.values())
layer.release();
_templates.clear();
if (_node!=null)
_node.release();
for (Layer layer : _instances.values())
layer.release();
_instances.clear();
super.doStop();
}
/* ------------------------------------------------------------ */
protected synchronized void updateLayers(Set<String> layerURIs)
{
_loading=System.currentTimeMillis();
for (String ruri: layerURIs)
{
try
{
// Decompose the name
File directory;
File archive;
File origin = new File(new URI(_scanDir.toURI()+ruri));
String name=origin.getName();
Monitor monitor = Monitor.valueOf(origin.getParentFile().getName().toUpperCase(Locale.ENGLISH));
String ext=".war";
// check directory vs archive
if (origin.isDirectory() || !origin.exists() && !ruri.toLowerCase(Locale.ENGLISH).endsWith(ext))
{
// directories have priority over archives
directory=origin;
archive=new File(directory.toString()+ext);
}
else
{
// check extension name
if (!ruri.toLowerCase(Locale.ENGLISH).endsWith(ext))
continue;
name=name.substring(0,name.length()-4);
archive=origin;
directory=new File(new URI(_scanDir.toURI()+ruri.substring(0,ruri.length()-4)));
// Look to see if directory exists
if (directory.exists())
{
__log.info("Directory exists, ignoring change to {}",ruri);
continue;
}
}
Layer layer=null;
switch(monitor)
{
case WEBAPPS:
if (origin.exists())
layer=loadWebapp(name,origin);
else
{
removeWebapp(name);
if (origin==directory && archive.exists())
layer=loadWebapp(name,archive);
}
break;
case TEMPLATES:
if (origin.exists())
layer=loadTemplate(name,origin);
else
{
removeTemplate(name);
if (origin==directory && archive.exists())
layer=loadTemplate(name,archive);
}
break;
case NODES:
if (name.equalsIgnoreCase(_nodeName))
{
if (origin.exists())
layer=loadNode(origin);
else
{
removeNode();
if (origin==directory && archive.exists())
layer=loadNode(archive);
}
}
break;
case INSTANCES:
if (origin.exists())
layer=loadInstance(name,origin);
else
{
removeInstance(name);
if (origin==directory && archive.exists())
layer=loadInstance(name,archive);
}
break;
}
if (layer!=null)
__log.info("loaded {}",layer.getLoadedKey());
}
catch(Exception e)
{
__log.warn(e);
}
}
redeploy();
// Release removed layers
for (Layer layer : _removedLayers)
{
if (layer!=null)
{
__log.info("unload {}",layer.getLoadedKey());
layer.release();
}
}
_removedLayers.clear();
if (__log.isDebugEnabled())
{
System.err.println("updated:");
System.err.println("java:"+javaRootURLContext.getRoot().dump());
System.err.println("local:"+localContextRoot.getRoot().dump());
if (getDeploymentManager()!=null && getDeploymentManager().getServer()!=null)
System.err.println(getDeploymentManager().getServer().dump());
}
}
/* ------------------------------------------------------------ */
protected File tmpdir(String name,String suffix) throws IOException
{
File dir=_tmpDir;
if (dir==null || !dir.isDirectory() || !dir.canWrite())
{
dir=new File(_scanDir,"tmp");
if (!dir.isDirectory() || !dir.canWrite())
dir=null;
}
File tmp = File.createTempFile(name+"_","."+suffix,dir);
tmp=tmp.getCanonicalFile();
if (tmp.exists())
IO.delete(tmp);
tmp.mkdir();
tmp.deleteOnExit();
return tmp;
}
/* ------------------------------------------------------------ */
/**
* Walks the defined webapps, templates, nodes and instances to
* determine what should be deployed, then adjust reality to match.
*/
protected void redeploy()
{
Map<String,Template> templates = new ConcurrentHashMap<String,Template>();
// Check for duplicate templates
for (Template template : _templates.values())
{
Template other=templates.get(template.getTemplateName());
if (other!=null)
{
__log.warn("Multiple Templates: {} & {}",template.getName(),other.getName());
if (other.getName().compareToIgnoreCase(template.getName())<=0)
continue;
}
templates.put(template.getTemplateName(),template);
}
// Match webapps to templates
for (Template template : templates.values())
{
String webappname=template.getClassifier();
if (webappname==null)
continue;
Webapp webapp = _webapps.get(webappname);
if (webapp==null)
{
__log.warn("No webapp found for template: {}",template.getName());
templates.remove(template.getTemplateName());
}
else
{
template.setWebapp(webapp);
}
}
// Match instance to templates and check if what needs to be deployed or undeployed.
Set<String> deployed = new HashSet<String>();
List<Instance> deploy = new ArrayList<Instance>();
for (Instance instance : _instances.values())
{
Template template=templates.get(instance.getTemplateName());
instance.setTemplate(template);
if (template!=null)
{
String key=instance.getInstanceKey();
App app = _deployed.get(key);
if (app==null)
deploy.add(instance);
else
deployed.add(key);
}
}
// Look for deployed apps that need to be undeployed
List<String> undeploy = new ArrayList<String>();
for (String key : _deployed.keySet())
{
if (!deployed.contains(key))
undeploy.add(key);
}
// Do the undeploys
for (String key : undeploy)
{
App app = _deployed.remove(key);
if (app!=null)
{
__log.info("Undeploy {}",key);
_deploymentManager.removeApp(app);
}
}
// ready the deploys
for (Instance instance : deploy)
{
String key=instance.getInstanceKey();
OverlayedApp app = new OverlayedApp(_deploymentManager,this,key,instance);
_deployed.put(key,app);
}
// Remove unused Shared stuff
Set<String> sharedKeys = new HashSet<String>(_shared.keySet());
for (OverlayedApp app : _deployed.values())
{
Instance instance = app.getInstance();
sharedKeys.remove(instance.getSharedKey());
}
for (String sharedKey: sharedKeys)
{
__log.debug("Remove "+sharedKey);
TemplateContext shared=_shared.remove(sharedKey);
if (shared!=null)
{
try
{
shared.stop();
}
catch(Exception e)
{
__log.warn(e);
}
shared.destroy();
}
}
// Do the deploys
for (Instance instance : deploy)
{
String key=instance.getInstanceKey();
OverlayedApp app = _deployed.get(key);
__log.info("Deploy {}",key);
_deploymentManager.addApp(app);
}
}
/* ------------------------------------------------------------ */
protected void removeInstance(String name)
{
_removedLayers.add(_instances.remove(name));
}
/* ------------------------------------------------------------ */
protected Instance loadInstance(String name, File origin)
throws IOException
{
Instance instance=new Instance(name,origin);
_removedLayers.add(_instances.put(name,instance));
return instance;
}
/* ------------------------------------------------------------ */
protected void removeNode()
{
if (_node!=null)
_removedLayers.add(_node);
_node=null;
}
/* ------------------------------------------------------------ */
protected Node loadNode(File origin)
throws IOException
{
if (_node!=null)
_removedLayers.add(_node);
_node=new Node(_nodeName,origin);
return _node;
}
/* ------------------------------------------------------------ */
protected void removeTemplate(String name)
{
_removedLayers.add(_templates.remove(name));
}
/* ------------------------------------------------------------ */
protected Template loadTemplate(String name, File origin)
throws IOException
{
Template template=new Template(name,origin);
_removedLayers.add(_templates.put(name,template));
return template;
}
protected void removeWebapp(String name)
{
_removedLayers.add(_webapps.remove(name));
}
/* ------------------------------------------------------------ */
protected Webapp loadWebapp(String name, File origin)
throws IOException
{
Webapp webapp = new Webapp(name,origin);
_removedLayers.add(_webapps.put(name,webapp));
return webapp;
}
/* ------------------------------------------------------------ */
private static List<Resource> getLayers(Layer... layers)
{
List<Resource> resources = new ArrayList<Resource>();
for (Layer layer: layers)
{
if (layer==null)
continue;
Resource resource = layer.getBaseResource();
if (resource.exists())
resources.add(resource);
}
return resources;
}
/* ------------------------------------------------------------ */
private static List<Resource> getLayeredResources(String path, Layer... layers)
{
List<Resource> resources = new ArrayList<Resource>();
for (Layer layer: layers)
{
if (layer==null)
continue;
Resource resource = layer.getResource(path);
if (resource.exists())
resources.add(resource);
}
return resources;
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
class Layer
{
private final String _name;
private final File _origin;
private final long _loaded=_loading;
private final Resource _resourceBase;
private final boolean _resourceBaseIsCopy;
public Layer(String name, File origin)
throws IOException
{
super();
_name = name;
_origin = origin;
Resource resource = Resource.newResource(origin.toURI());
if (resource.isDirectory())
{
if (_copydir)
{
File tmp=tmpdir(name,"extract");
__log.info("Extract {} to {}",origin,tmp);
IO.copyDir(origin,tmp);
_resourceBase=Resource.newResource(tmp.toURI());
_resourceBaseIsCopy=true;
}
else
{
_resourceBase=resource;
_resourceBaseIsCopy=false;
}
}
else
{
Resource jar = JarResource.newJarResource(resource);
File tmp=tmpdir(name,"extract");
__log.info("Extract {} to {}",jar,tmp);
jar.copyTo(tmp);
_resourceBase=Resource.newResource(tmp.toURI());
_resourceBaseIsCopy=true;
}
}
public String getName()
{
return _name;
}
public File getOrigin()
{
return _origin;
}
public long getLoaded()
{
return _loaded;
}
public Resource getBaseResource()
{
return _resourceBase;
}
public Resource getResource(String path)
{
try
{
return getBaseResource().addPath(path);
}
catch(Exception e)
{
__log.warn(e);
}
return null;
}
public String getLoadedKey()
{
return _name+"@"+_loaded;
}
public void release()
{
if (_resourceBaseIsCopy)
{
try
{
File file = _resourceBase.getFile();
if (file!=null)
IO.delete(file);
}
catch(Exception e)
{
__log.warn(e);
}
}
}
public String toString()
{
return getLoadedKey();
}
}
class Webapp extends Layer
{
public Webapp(String name, File origin) throws IOException
{
super(name,origin);
}
}
class Overlay extends Layer
{
public Overlay(String name, File origin) throws IOException
{
super(name,origin);
}
public Resource getContext()
{
return getResource(OVERLAY_XML);
}
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
class Node extends Overlay
{
public Node(String name, File origin) throws IOException
{
super(name,origin);
}
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
class ClassifiedOverlay extends Overlay
{
private final String _templateName;
private final String _classifier;
public ClassifiedOverlay(String name, File origin) throws IOException
{
super(name,origin);
int l=1;
int e=name.indexOf('=');
if (e<0)
{
l=2;
e=name.indexOf("--");
}
_templateName=e>=0?name.substring(0,e):name;
_classifier=e>=0?name.substring(e+l):null;
}
public String getTemplateName()
{
return _templateName;
}
public String getClassifier()
{
return _classifier;
}
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
class Template extends ClassifiedOverlay
{
private Webapp _webapp;
public Webapp getWebapp()
{
return _webapp;
}
public void setWebapp(Webapp webapp)
{
_webapp = webapp;
}
public Template(String name, File origin) throws IOException
{
super(name,origin);
}
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
class Instance extends ClassifiedOverlay
{
Template _template;
String _sharedKey;
public Instance(String name, File origin) throws IOException
{
super(name,origin);
if (getClassifier()==null)
throw new IllegalArgumentException("Instance without '=':"+name);
}
public void setSharedKey(String key)
{
_sharedKey=key;
}
public String getSharedKey()
{
return _sharedKey;
}
public void setTemplate(Template template)
{
_template=template;
}
public Template getTemplate()
{
return _template;
}
public String getInstanceKey()
{
return
(_template.getWebapp()==null?"":_template.getWebapp().getLoadedKey())+"|"+
_template.getLoadedKey()+"|"+
(_node==null?"":_node.getLoadedKey())+"|"+
getLoadedKey();
}
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
static class OverlayedApp extends App
{
final Instance _instance;
public OverlayedApp(DeploymentManager manager, AppProvider provider, String originId, Instance instance)
{
super(manager,provider,originId);
_instance=instance;
}
public Instance getInstance()
{
return _instance;
}
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
private final class ParameterExpander implements ServletContextListener
{
private final Map<String, String> _params;
private final ContextHandler _ctx;
private ParameterExpander(Map<String, String> params, ContextHandler ctx)
{
_params = params;
_ctx = ctx;
}
public void contextInitialized(ServletContextEvent sce)
{
Enumeration<String> e=_ctx.getInitParameterNames();
while (e.hasMoreElements())
{
String name = e.nextElement();
_ctx.setInitParameter(name,expandParameter(_ctx.getInitParameter(name)));
}
ServletHandler servletHandler = _ctx.getChildHandlerByClass(ServletHandler.class);
if (servletHandler!=null)
{
List<Holder<?>> holders = new ArrayList<Holder<?>>();
if (servletHandler.getFilters()!=null)
holders.addAll(Arrays.asList(servletHandler.getFilters()));
if (servletHandler.getHandler()!=null)
holders.addAll(Arrays.asList(servletHandler.getServlets()));
for (Holder<?> holder: holders)
{
e=holder.getInitParameterNames();
while (e.hasMoreElements())
{
String name = e.nextElement();
holder.setInitParameter(name,expandParameter(holder.getInitParameter(name)));
}
}
}
}
private String expandParameter(String value)
{
int i=0;
while (true)
{
int open=value.indexOf("${",i);
if (open<0)
return value;
int close=value.indexOf("}",open);
if (close<0)
return value;
String param = value.substring(open+2,close);
if (_params.containsKey(param))
{
String tmp=value.substring(0,open)+_params.get(param);
i=tmp.length();
value=tmp+value.substring(close+1);
}
else
i=close+1;
}
}
public void contextDestroyed(ServletContextEvent sce)
{
}
}
}