/*
* JBoss, the OpenSource EJB server
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jboss.deployment;
import java.net.URL;
import java.net.MalformedURLException;
import java.net.URLClassLoader;
import java.io.File;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.Hashtable;
import java.util.Vector;
import java.util.ArrayList;
import java.util.StringTokenizer;
import java.util.Collection;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipEntry;
import java.util.Enumeration;
import javax.management.MBeanServer;
import javax.management.MBeanException;
import javax.management.JMException;
import javax.management.ObjectName;
import javax.management.RuntimeMBeanException;
import javax.management.RuntimeErrorException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.jboss.deployment.DeploymentException;
import org.jboss.ejb.ContainerFactoryMBean;
import org.jboss.logging.Logger;
import org.jboss.metadata.XmlFileLoader;
import org.jboss.util.MBeanProxy;
import org.jboss.util.ServiceMBeanSupport;
/** J2eeDeployer allows to deploy single EJB.jars as well as Web.wars
* (if a Servlet Container is present) or even Application.ears.
* The deployment is done by determining the file type of the given url.
* The file must be a valid zip file or a directory and must contain either a META-INF/ejb-jar.xml
* or META-INF/application.xml or a WEB-INF/web.xml file.
* Depending on the file type, the whole file (EJBs, WARs)
* or only the relevant packages (EAR) becoming downloaded.
* replacing alternative DDs and validation is not yet implementet!
* The uploaded files are getting passed through to the responsible deployer
* (ContainerFactory for JBoss and EmbededTomcatService for Tomcat).
*
* @author Daniel Schulze
* @author Toby Allsopp (toby.allsopp@peace.com)
* @author Scott_Stark@displayscape.com
* @version $Revision: 1.31.2.5 $
*/
public class J2eeDeployer
extends ServiceMBeanSupport
implements J2eeDeployerMBean
{
// Constants -----------------------------------------------------
public File DEPLOYMENT_DIR = null;//"/home/deployment"; // default? MUST BE ABSOLUTE PATH!!!
public static String CONFIG = "deployment.cfg";
public static final int EASY = 0;
public static final int RESTRICTIVE = 1;
// Attributes ----------------------------------------------------
// my server to lookup for the special deployers
protected MBeanServer server;
/** */
protected String name;
/** The name of the EJB jar deployer */
protected ObjectName jarDeployer;
/** The name of the Web war deployer */
protected ObjectName warDeployer;
/** The InstallerFactory which installs J2EE module deployments */
protected InstallerFactory installer;
String jarDeployerName;
String warDeployerName;
int classpathPolicy = EASY;
// Static --------------------------------------------------------
/** only for testing...*/
public static void main(String[] _args) throws Exception
{
new J2eeDeployer().deploy(_args[0]);
}
// Constructors --------------------------------------------------
public J2eeDeployer()
{
this("Default", "EJB:service=ContainerFactory", ":service=EmbeddedTomcat");
}
public J2eeDeployer(String _name, String jarDeployerName, String warDeployerName)
{
setDeployerName(_name);
setJarDeployerName(jarDeployerName);
setWarDeployerName(warDeployerName);
}
public void setDeployerName(String name)
{
name = name.equals("") ? "" : " "+name;
this.name = name;
}
public String getDeployerName()
{
return name.trim();
}
public void setJarDeployerName(String jarDeployerName)
{
this.jarDeployerName = jarDeployerName;
}
public String getJarDeployerName()
{
return jarDeployerName;
}
public void setWarDeployerName(String warDeployerName)
{
this.warDeployerName = warDeployerName;
}
public String getWarDeployerName()
{
return warDeployerName;
}
// Public --------------------------------------------------------
public FilenameFilter getDeployableFilter()
{
return InstallerFactory.getDeployableFilter();
}
/** Deploys the given URL independent if it is a EJB.jar, Web.war
* or Application.ear. In case of already deployed, it performes a
* redeploy.
* @param _url the url (file or http) to the archiv to deploy
* @throws MalformedURLException in case of a malformed url
* @throws J2eeDeploymentException if something went wrong...
* @throws IOException if trouble while file download occurs
*/
public void deploy(String _url) throws MalformedURLException, IOException, J2eeDeploymentException
{
URL url = new URL(_url);
// factored out for subclass access
ObjectName lCollector = createCollectorName();
// undeploy first if it is a redeploy
try
{
undeploy(_url);
// Remove application data by its id
// factored out for subclass access
removeFromCollector(_url,lCollector);
}
catch (Exception _e)
{}
// now try to deploy
log.info("Deploy J2EE application: " + _url);
Deployment d = installApplication(url);
try
{
// factored out for subclass access
startApplication(d);
log.info("J2EE application: " + _url + " is deployed.");
}
catch (Exception _e)
{
try
{
stopApplication(d);
}
catch (Exception _e2)
{
log.error("unable to stop application "+d.name+": "+_e2);
}
finally
{
try
{
uninstallApplication(_url);
}
catch (Exception _e3)
{
log.error("unable to uninstall application "+d.name+": "+_e3);
}
}
if (_e instanceof J2eeDeploymentException)
{
throw (J2eeDeploymentException)_e;
}
else
{
log.error("fatal error", _e);
throw new J2eeDeploymentException("fatal error: "+_e);
}
}
}
/** creation of collector name factored out.
* @author schaefera
* @author cgjung
*/
protected ObjectName createCollectorName()
{
try
{
return new ObjectName( "Management", "service", "Collector" );
}
catch( Exception e )
{
return null;
}
}
/** report of removal to data collector factored out for subclass access
* a try/catch for dealing with an uninstalled collector has been added.
* @author schaefera
* @author cgjung
*/
protected void removeFromCollector(String _url, ObjectName lCollector)
{
try
{
server.invoke(
lCollector,
"removeApplication",
new Object[]
{ _url },
new String[]
{ "java.lang.String" }
);
} catch(Exception e)
{
log.info("Report of undeployment of J2EE application: " + _url + " could not be reported.");
}
}
/** Undeploys the given URL (if it is deployed).
* Actually only the file name is of interest, so it dont has to be
* an URL to be undeployed, the file name is ok as well.
* @param _url the url to to undeploy
* @throws MalformedURLException in case of a malformed url
* @throws J2eeDeploymentException if something went wrong (but should have removed all files)
* @throws IOException if file removement fails
*/
public void undeploy(String _app) throws IOException, J2eeDeploymentException
{
Deployment d = installer.findDeployment(_app);
if (d == null)
throw new J2eeDeploymentException("The application \""+name+"\" has not been deployed.");
try
{
stopApplication(d);
}
catch (J2eeDeploymentException _e)
{
throw _e;
}
finally
{
uninstallApplication(d);
}
}
/** Checks if the given URL is currently deployed or not.
* Actually only the file name is of interest, so it dont has to be
* an URL to be undeployed, the file name is ok as well.
* @param _url the url to to check
* @return true if _url is deployed
* @throws MalformedURLException in case of a malformed url
* @throws J2eeDeploymentException if the app seems to be deployed, but some of its modules
* are not.
*/
public boolean isDeployed(String _url) throws MalformedURLException, J2eeDeploymentException
{
boolean result = false;
Deployment d = installer.findDeployment(_url);
if (d != null)
{
result = checkApplication(d);
}
return result;
}
// ServiceMBeanSupport overrides ---------------------------------
public String getName()
{
return "J2EE Deployer" + this.name;
}
protected ObjectName getObjectName(MBeanServer server, ObjectName name)
throws javax.management.MalformedObjectNameException
{
this.server = server;
return name == null ? new ObjectName(OBJECT_NAME+this.name) : name;
}
/** */
protected void initService()
throws Exception
{
URL tmpDirUrl = getClass().getResource("/tmp.properties");
if( tmpDirUrl == null )
throw new IOException("Failed to get /tmp.properties URL; Temporary directory does not exist!");
//check if the deployment dir was set meaningful
File dir = new File(new File(tmpDirUrl.getFile()).getParentFile(), "deploy/"+getDeployerName());
if (!dir.exists() && !dir.mkdirs())
throw new IOException("Temporary directory \""+dir.getCanonicalPath()+"\" does not exist!");
installer = new InstallerFactory(dir, log);
// Save JMX name of the deployers
jarDeployer = new ObjectName(jarDeployerName);
warDeployer= new ObjectName(warDeployerName);
}
/** */
protected void startService()
throws Exception
{
if (!warDeployerAvailable())
log.info("No web container found - only EJB deployment available...");
// clean up the deployment directory since on some Windowz the file removement
// during runtime doesnt work...
log.info("Cleaning up deployment directory");
installer.unclutter();
}
/** undeploys all deployments */
protected void stopService()
{
log.info("Undeploying all applications.");
Deployment[] deps = installer.getDeployments();
int count = 0;
for (int i = 0, l = deps.length; i
* This means download the needed packages do some validation...
* Validation and do some other things is not yet implemented
* better be protected for subclassing
* @param _downloadUrl the url that points to the app to install
* @throws IOException if the download fails
* @throws J2eeDeploymentException if the given package is somehow inconsistent
*/
protected Deployment installApplication(URL _downloadUrl) throws IOException, J2eeDeploymentException
{
return installer.install(_downloadUrl);
}
/** Deletes the file tree of the specified application.
* better be protected for subclassing
* @param _name the directory (DEPLOYMENT_DIR/<_name> to remove recursivly
* @throws IOException if something goes wrong
*/
protected void uninstallApplication(String _pattern) throws IOException
{
Deployment d = installer.findDeployment(_pattern);
if (d != null)
uninstallApplication(d);
}
protected void uninstallApplication(Deployment _d) throws IOException
{
log.info("Destroying application " + _d.name);
installer.uninstall(_d);
}
/** Starts the successful downloaded deployment.
* Means the modules are deployed by the responsible container deployer
* better be protected for subclassing
* @param _d the deployment to start
* @throws J2eeDeploymentException if an error occures for one of these
* modules
*/
protected void startApplication(Deployment _d) throws J2eeDeploymentException
{
// save the old classloader
ClassLoader oldCl = Thread.currentThread().getContextClassLoader();
// set the context classloader for this application
createContextClassLoader(_d);
// save the application classloader for later
ClassLoader appCl = Thread.currentThread().getContextClassLoader();
DeploymentNotification evt = new DeploymentNotification( this, nextSequenceNumber(),
_d, DeploymentNotification.PREDEPLOY );
sendNotification(evt);
// module deployment factored out for subclass
// access
// redirect all modules to the responsible deployers
startModules(_d,appCl,oldCl);
evt = new DeploymentNotification( this, nextSequenceNumber(),
_d, DeploymentNotification.POSTDEPLOY );
sendNotification(evt);
}
/** factored out method to start individual modules and reinstall
* context classloader afterwards
*/
protected void startModules(Deployment _d, ClassLoader appCl, ClassLoader oldCl) throws J2eeDeploymentException
{
Deployment.Module m = null;
String moduleName = null;
String message;
try
{
// Deploy the ejb modules
moduleName = _d.name;
Vector tmp = new java.util.Vector();
Iterator it = _d.ejbModules.iterator();
while( it.hasNext() )
{
m = (Deployment.Module) it.next();
tmp.add( m.localUrls.firstElement().toString() );
}
String[] jarUrls = new String[ tmp.size() ];
tmp.toArray( jarUrls );
// Call the ContainerFactory that is loaded in the JMX server
server.invoke(jarDeployer, "deploy",
new Object[]
{ _d.localUrl.toString(), jarUrls, moduleName },
new String[]
{ String.class.getName(), String[].class.getName(), String.class.getName() } );
// Deploy the web application modules
it = _d.webModules.iterator();
if (it.hasNext() && !warDeployerAvailable())
throw new J2eeDeploymentException("application contains war files but no web container available");
while( it.hasNext() )
{
m = (Deployment.Module)it.next();
moduleName = m.name;
log.info("Starting module " + moduleName);
// Call the TomcatDeployer that is loaded in the JMX server
server.invoke(warDeployer, "deploy",
new Object[]
{ m.webContext, m.localUrls.firstElement().toString()}, new String[]
{ "java.lang.String", "java.lang.String" });
// since tomcat changes the context classloader...
Thread.currentThread().setContextClassLoader(appCl);
}
}
catch (MBeanException e)
{
log.error("Starting "+moduleName+" failed!");
e.getTargetException().printStackTrace();
throw new J2eeDeploymentException("Error while starting "+moduleName+": " + e.getTargetException().getMessage(), e.getTargetException());
}
catch (RuntimeErrorException e)
{
log.error("Starting "+moduleName+" failed!");
e.getTargetError().printStackTrace();
throw new J2eeDeploymentException("Error while starting "+moduleName+": " + e.getTargetError().getMessage(), e.getTargetError());
}
catch (RuntimeMBeanException e)
{
log.error("Starting "+moduleName+" failed!");
e.getTargetException().printStackTrace();
throw new J2eeDeploymentException("Error while starting "+moduleName+": " + e.getTargetException().getMessage(), e.getTargetException());
}
catch (JMException e)
{
log.error("Starting failed!");
e.printStackTrace();
throw new J2eeDeploymentException("Fatal error while interacting with deployer MBeans... " + e.getMessage());
}
finally
{
Thread.currentThread().setContextClassLoader(oldCl);
}
}
/** Stops a running deployment.
* Means the modules are undeployed by the responsible container deployer
* better protected for subclassing
* @param _d the deployment to stop
* @throws J2eeDeploymentException if an error occures for one of these
* modules
*/
protected void stopApplication(Deployment _d) throws J2eeDeploymentException
{
// save the old classloader, tomcat replaces my classloader somehow?!
ClassLoader oldCl = Thread.currentThread().getContextClassLoader();
StringBuffer error = new StringBuffer();
DeploymentNotification evt = new DeploymentNotification( this, nextSequenceNumber(),
_d, DeploymentNotification.PREUNDEPLOY );
sendNotification(evt);
// stop the web modules
if( warDeployer != null )
{
for( Iterator webModules = _d.webModules.iterator(); webModules.hasNext(); )
{
Deployment.Module m = (Deployment.Module)webModules.next();
stopModule( warDeployer, m.name, m.localUrls.firstElement().toString(), error );
}
}
else
{
// in case we are not running with tomcat
// should only happen for tomcat (i=1)
log.warn("Cannot find web container");
}
// stop the jar modules (the ContainerFactory is responsible for undeploying
// all jars associated w/ a given application)
stopModule( jarDeployer, _d.name, _d.localUrl.toString(), error );
if (!error.toString().equals("")) // there was at least one error...
throw new J2eeDeploymentException("Error(s) on stopping application "+_d.name+":\n"+error.toString());
// restore the classloader
Thread.currentThread().setContextClassLoader(oldCl);
evt = new DeploymentNotification( this, nextSequenceNumber(),
_d, DeploymentNotification.POSTUNDEPLOY );
sendNotification(evt);
}
private void stopModule( ObjectName container, String moduleName, String moduleUrl, StringBuffer error )
{
try
{
// Call the ContainerFactory/EmbededTomcat that is loaded in the JMX server
Object result = server.invoke(container, "isDeployed",
new Object[]
{ moduleUrl }, new String[]
{ "java.lang.String" });
if (((Boolean)result).booleanValue())
{
log.info("Stopping module " + moduleName);
server.invoke(container, "undeploy",
new Object[]
{ moduleUrl }, new String[]
{ "java.lang.String" });
}
else
log.info("Module " + moduleName + " is not running");
}
catch (MBeanException _mbe)
{
log.error("Unable to stop module " + moduleName + ": " + _mbe.getTargetException().getMessage());
error.append("Unable to stop module " + moduleName + ": " + _mbe.getTargetException().getMessage());
error.append("/n");
}
catch (JMException _jme)
{
log.error("Unable to stop module " + moduleName + ": " + _jme.getMessage());
error.append("Unable to stop module " + moduleName + ": fatal error while calling " + container + ": " + _jme.getMessage());
error.append("/n");
}
}
/** Checks the Deplyment if it is correctly deployed.
* @param app to check
* @throws J2eeDeploymentException if some inconsistency in the deployment is
* detected
*/
private boolean checkApplication(Deployment _d) throws J2eeDeploymentException
{
boolean result = false;
int count = 0;
int others = 0;
// Call the ContainerFactory/EmbededTomcat that is loaded in the JMX server
Object o = checkModule( jarDeployer, _d.name, _d.localUrl.toString() );
if( o == null )
++others;
else
result = ((Boolean) o).booleanValue();
if (warDeployer != null )
{
for( Iterator webModules = _d.webModules.iterator(); webModules.hasNext(); )
{
Deployment.Module m = (Deployment.Module)webModules.next();
o = checkModule( warDeployer, m.name, m.localUrls.firstElement().toString() );
if (o == null) // had an exception
++others;
else if (count++ == 0) // first module -> set state
result = ((Boolean)o).booleanValue();
else if (result != ((Boolean)o).booleanValue()) // only if differs from state
++others;
}
}
else
{
// in case we are not running with tomcat
log.warn("Cannot find web container");
}
if (others > 0)
// there was at least one error...
throw new J2eeDeploymentException("Application "+_d.name+" is not correctly deployed! ("+
(result ? count-others : others)+
" modules are running "+
(result ? others : count-others)+
" are not)");
return result;
}
private Object checkModule( ObjectName container, String moduleName, String moduleUrl )
{
try
{
log.info("Checking module " + moduleName);
// Call the ContainerFactory/EmbededTomcat that is loaded in the JMX server
return server.invoke(container, "isDeployed",
new Object[]
{ moduleUrl }, new String[]
{ "java.lang.String" });
}
catch (MBeanException _mbe)
{
log.error("Error while checking module " + moduleName + ": " + _mbe.getTargetException().getMessage());
return null;
}
catch (JMException _jme)
{
log.error("Fatal error while checking module " + moduleName + ": " + _jme.getMessage());
return null;
}
}
/** tests if the web container deployer is available
* better be protected for subclassing
*/
protected boolean warDeployerAvailable()
{
return server.isRegistered(warDeployer);
}
/**
* creates an application class loader for this deployment
* this class loader will be shared between jboss and tomcat via the contextclassloader. May throw
* a J2eeDeploymentException to indicate problems stting up the classloader.
* should be protected in order to allow reasonable subclassing. Needs an
* exception in case that you do more sophisticated meta-data installations.
*
*/
protected void createContextClassLoader(Deployment deployment) throws J2eeDeploymentException
{
// get urls we want all classloaders of this application to share
URL[] urls = new URL[deployment.commonUrls.size()];
for (int i = 0, l = deployment.commonUrls.size(); i < l; ++i)
urls[i] = (URL)deployment.commonUrls.elementAt(i);
// create classloader
ClassLoader parent = Thread.currentThread().getContextClassLoader();
URLClassLoader appCl = new URLClassLoader(urls, parent);
// set it as the context class loader for the deployment thread
Thread.currentThread().setContextClassLoader(appCl);
}
}