/**
* Helios, OpenSource Monitoring
* Brought to you by the Helios Development Group
*
* Copyright 2007, Helios Development Group 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.helios.apmrouter.deployer;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicLong;
import javax.management.ObjectName;
import org.apache.log4j.Logger;
import org.helios.apmrouter.jmx.JMXHelper;
import org.helios.apmrouter.spring.ctx.ApplicationContextService;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.core.io.UrlResource;
/**
* <p>Title: ApplicationContextDeployer</p>
* <p>Description: Manages the deployment/undeployment of hot deployed application contexts</p>
* <p>Company: Helios Development Group LLC</p>
* @author Whitehead (nwhitehead AT heliosdev DOT org)
* <p><code>org.helios.apmrouter.deployer.ApplicationContextDeployer</code></p>
*/
public class ApplicationContextDeployer {
/** Instance logger */
protected final Logger log = Logger.getLogger(getClass());
/** Indicates if the default module hot deploy lib directory class loading should be disabled. By default it is enabled */
protected final boolean disableHotDirLibs;
/** Hot Deployed Context ID serial */
private static final AtomicLong CTX_SERIAL = new AtomicLong();
/**
* Creates a new ApplicationContextDeployer
* @param disableHotDirLibs Indicates if the default module hot deploy lib directory class loading should be disabled
*/
protected ApplicationContextDeployer(boolean disableHotDirLibs) {
this.disableHotDirLibs = disableHotDirLibs;
}
/**
* Hot deploys the application context defined in the passed file
* @param parent The parent context
* @param taskExecutor The application event multicaster task executor
* @param innerListener The application listener that listens exclusively on hot deployed contexts
* @param fe The file event referencing a new or modified file
* @return the deployed application context
*/
protected GenericApplicationContext deploy(ApplicationContext parent, Executor taskExecutor, SmartApplicationListener innerListener, FileEvent fe) {
try {
log.info("Deploying AppCtx [" + fe.getFileName() + "]");
File f = new File(fe.getFileName());
if(!f.canRead()) throw new Exception("Cannot read file [" + fe + "]", new Throwable());
HotDeployerClassLoader cl = findClassLoader(f);
cl.init();
StringBuilder b = new StringBuilder("\nHotDeployerClassLoader URLs for [").append(fe.getFileName()).append("]\t [");
for(URL url: cl.getURLs()) {
b.append("\n\t").append(url);
}
b.append("\n]");
log.info(b);
final ClassLoader current = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(cl);
String id = "HotDeployedContext#" + CTX_SERIAL.incrementAndGet() + "(" + f.getAbsolutePath() + ")";
HotDeployedApplicationContext appCtx = new HotDeployedApplicationContext(taskExecutor, id);
appCtx.addApplicationListener(innerListener);
//appCtx.setClassLoader(findClassLoader(f));
appCtx.setDisplayName(f.getAbsolutePath());
appCtx.setParent(parent);
appCtx.load(new UrlResource(f.toURI().toURL()));
for(String beanName: appCtx.getBeanDefinitionNames()) {
BeanDefinition beanDef = appCtx.getBeanDefinition(beanName);
if(HotDeployerClassLoader.class.getName().equals(beanDef.getBeanClassName())) {
appCtx.removeBeanDefinition(beanName);
}
}
// Add any located wars
boolean hasWars = false;
for(String warFileName: cl.getWars()) {
File warFile = new File(warFileName);
WARDeployer.deploy(appCtx, warFile);
hasWars = true;
}
ObjectName on = JMXHelper.objectName(ApplicationContextService.HOT_OBJECT_NAME_PREF + ObjectName.quote(f.getAbsolutePath()));
ApplicationContextService.register(on, appCtx);
appCtx.refresh();
//jimmyTheJettyWebApps(appCtx, cl);
if(hasWars) {
//jiggleTheHandlers(appCtx, cl);
}
return appCtx;
} finally {
Thread.currentThread().setContextClassLoader(current);
}
} catch (Throwable ex) {
log.error("Failed to deploy application context [" + fe + "]", ex);
throw new RuntimeException("Failed to deploy application context [" + fe + "]", ex);
}
}
/**
* Stops and starts the handler collection. For some reason, the webapps don't start correctly without this.
* @param appCtx The app context the handlers are deployed in
* @param cl The class loader
*/
protected void jiggleTheHandlers(GenericXmlApplicationContext appCtx, ClassLoader cl) {
try {
Class<?> handlerClass = Class.forName("org.eclipse.jetty.server.handler.HandlerCollection", true, cl);
Object handler = appCtx.getBean(handlerClass);
handlerClass.getMethod("stop").invoke(handler);
log.info("Stopped Handler");
handlerClass.getMethod("start").invoke(handler);
log.info("Started Handler");
} catch (Exception ex) {
log.error("Failed to jiggle the HandlerCollection", ex);
}
}
protected void jimmyTheJettyWebApps(GenericXmlApplicationContext appCtx, ClassLoader cl) {
final ClassLoader current = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(cl);
//Class<?> webAppClazz = Class.forName("org.eclipse.jetty.annotations.AnnotationConfiguration", true, cl);
if(!appCtx.containsBean("JettyAnnotations")) return;
Object annotationConfiguration = appCtx.getBean("JettyAnnotations");
if(annotationConfiguration!=null) {
Method configMethod = null; //annotationConfiguration.getClass().getDeclaredMethod("configure", webAppClazz);
Class<?> webAppClass = null;
for(Method m: annotationConfiguration.getClass().getDeclaredMethods()) {
if(m.getName().equals("configure")) {
configMethod = m;
webAppClass = configMethod.getParameterTypes()[0];
break;
}
}
if(configMethod==null) throw new Exception("No Config Method Found");
for(Object webApp: appCtx.getBeansOfType(webAppClass).values()) {
configMethod.invoke(annotationConfiguration, webApp);
}
}
} catch (Exception ex) {
ex.printStackTrace(System.err);
} finally {
Thread.currentThread().setContextClassLoader(current);
}
}
/**
* Unregisters the application context mbean and closes the app context
* @param appCtx The app context to undeploy
*/
protected void undeploy(GenericApplicationContext appCtx) {
try {
ObjectName on = JMXHelper.objectName(ApplicationContextService.HOT_OBJECT_NAME_PREF + ObjectName.quote(appCtx.getDisplayName()));
if(JMXHelper.getHeliosMBeanServer().isRegistered(on)) {
JMXHelper.getHeliosMBeanServer().unregisterMBean(on);
}
} catch (Exception ex) {
log.warn("Failed to undeploy AppCtx MBean for [" + appCtx.getDisplayName() + "]", ex);
}
appCtx.close();
}
/**
* Creates a new GenericXmlApplicationContext configured by the passed XML file.
* Attempts to locate a {@link HotDeployerClassLoader} definition in the bean definitions.
* If one is found, it is instantiated and configured, then used as the GenericXmlApplicationContext's
* classloader. The bean definition is removed from the context before being returned since it is no longer needed.
* @param xmlFile The file to inspect
* @return the created GenericXmlApplicationContet
* @throws Exception thrown on any error
*/
protected HotDeployerClassLoader findClassLoader(File xmlFile) throws Exception {
HotDeployerClassLoader cl = new HotDeployerClassLoader();
Set<String>[] locateds = getHotDeployAutoEntries(xmlFile);
cl.setClassPathEntries(locateds[0]);
cl.addWars(locateds[1]);
GenericXmlApplicationContext appCtx = new GenericXmlApplicationContext();
appCtx.load(new UrlResource(xmlFile.toURI().toURL()));
for(String beanName: appCtx.getBeanDefinitionNames()) {
BeanDefinition beanDef = appCtx.getBeanDefinition(beanName);
if(!HotDeployerClassLoader.class.getName().equals(beanDef.getBeanClassName())) {
appCtx.removeBeanDefinition(beanName);
}
}
appCtx.refresh();
Map<String, HotDeployerClassLoader> classLoaders = appCtx.getBeansOfType(HotDeployerClassLoader.class);
if(classLoaders != null) {
for(HotDeployerClassLoader hcl: classLoaders.values()) {
if(cl==null) {
cl = hcl;
} else {
cl.merge(hcl);
}
}
}
appCtx.close();
return cl;
}
/**
* Auto locates libraries and wars for the deploying app context
* @param xmlFile The hot xml file
* @return An array of two sets, one with located libs, the next with wars
*/
@SuppressWarnings({ "cast", "unchecked" })
protected Set<String>[] getHotDeployAutoEntries(File xmlFile) {
Set<String> entries = new HashSet<String>();
Set<String> wars = new HashSet<String>();
File libDir = new File(xmlFile.getParent(), xmlFile.getName().split("\\.")[0] + ".lib");
if(libDir.exists() && libDir.isDirectory()) {
log.info("Auto adding libs in application directory [" + libDir + "]");
for(File jar: libDir.listFiles()) {
if(jar.toString().toLowerCase().endsWith(".jar")) {
entries.add(jar.toString());
}
if(jar.toString().toLowerCase().endsWith(".war")) {
wars.add(jar.toString());
}
}
}
return ((Set<String>[])new Set[]{entries, wars});
}
}