/*
* Jopr Management Platform
* Copyright (C) 2005-2009 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2, as
* published by the Free Software Foundation, and/or the GNU Lesser
* General Public License, version 2.1, also as published by the Free
* Software Foundation.
*
* This program 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 General Public License and the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License
* and the GNU Lesser General Public License along with this program;
* if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.rhq.plugins.jbossas.util;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.HashSet;
import java.util.Set;
import javax.management.ObjectName;
import bsh.EvalError;
import bsh.Interpreter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mc4j.ems.connection.EmsConnection;
import org.mc4j.ems.connection.bean.EmsBean;
import org.mc4j.ems.connection.bean.attribute.EmsAttribute;
import org.mc4j.ems.connection.bean.operation.EmsOperation;
import org.rhq.plugins.jmx.util.ObjectNameQueryUtility;
/**
* Accesses the MainDeployer mbean to find the deployment files behind services.
*
* @author Greg Hinkle
* @author Heiko W. Rupp
*/
public class DeploymentUtility {
private static Log log = LogFactory.getLog(DeploymentUtility.class);
/**
* The object name of the JBoss main deployer MBean.
*/
protected static final String MAIN_DEPLOYER = "jboss.system:service=MainDeployer";
/**
* The name of the main deployer operation that is to be used to get the list of the modules are deployed - this is
* the Main Deployer operation name for JBossAS 4.x.
*/
private static final String LIST_DEPLOYED_MODULES_OP_NAME = "listDeployedModules";
/**
* The name of the main deployer operation that is to be used to get the list of the modules are deployed - this is
* the Main Deployer operation name for JBossAS 3.x.
*/
private static final String LIST_DEPLOYED_OP_NAME = "listDeployed";
private static final String JBOSS_WEB_MBEAN_NAME_TEMPLATE =
"jboss.web:J2EEApplication=none,J2EEServer=none,j2eeType=WebModule,name=%s";
protected static EmsOperation getListDeployedOperation(EmsConnection connection) {
EmsOperation retOperation;
EmsBean bean = connection.getBean(MAIN_DEPLOYER);
// first try the new operation name, used by JBossAS 3.2.8 and 4.x.
retOperation = bean.getOperation(LIST_DEPLOYED_MODULES_OP_NAME);
// if that doesn't exist, we are probably connected to a JBossAS 3.2.7 or earlier version
if (retOperation == null) {
retOperation = bean.getOperation(LIST_DEPLOYED_OP_NAME);
}
// if we still did not manage to find the operation name, let the caller handle this error condition
return retOperation;
}
/**
* This will attempt to find the deployment descriptor file where the ObjectName MBean was deployed.
*
* @param connection the connection to the JBoss instance
* @param objectName The objectname to look for
* @return the path to the file where the MBean was deployed, or <code>null</code> if it could not be found
*/
public static File getDescriptorFile(EmsConnection connection, String objectName) {
File retDescriptorFile = null;
// will contain information on all deployments
Collection deployed;
try {
deployed = getDeploymentInformations(connection);
}
catch (Exception e) {
return null;
}
/* The next code block is already executed within getDeploymentInformations()
so no point in repeating here
try {
EmsOperation operation = getListDeployedOperation(connection);
if (operation == null) {
throw new UnsupportedOperationException(
"The JBossAS instance is unsupported; it doesn't have a listDeployed operation");
}
deployed = (Collection) operation.invoke(new Object[0]);
} catch (Exception e) {
log.warn("Cannot determine the descriptor file name for service [" + objectName + "]. Cause: " + e);
return null;
}
*/
// ask our connection object to create an ObjectName for us as a java.lang.Object - we don't want to import ObjectName ourselves
Object ourObjectName = connection.buildObjectName(objectName);
// used by our BSH script to see if the current deployment's list of object names contains our object name
String testScriptObjectNameVariable = "ourObjectName";
String testScriptContainsOurObjectName = "sdi.mbeans.contains(" + testScriptObjectNameVariable + ")";
// find out which deployment was responsible for deploying our MBean (identified by objectName)
Interpreter i = new Interpreter();
for (Iterator it = deployed.iterator(); it.hasNext() && (retDescriptorFile == null);) {
Object sdi = it.next();
try {
i.set("sdi", sdi);
// this is the deployment descriptor file that we are currently examining;
// this is what will be returned if the MBean was configured in this file.
String file = i.eval("sdi.watch").toString();
if (file.startsWith("file:/")) {
file = file.substring(5);
}
// get the collection of MBeans that were deployed from the current deployment and
// see if our MBean object name is among them
i.set(testScriptObjectNameVariable, ourObjectName);
Boolean b = (Boolean) i.eval(testScriptContainsOurObjectName);
if (b) {
retDescriptorFile = new File(file); // found it! this is the file where the MBean was configured/deployed
break;
}
} catch (EvalError evalError) {
log.warn("Failed to determine if a deployment contains our mbean", evalError);
}
}
log.debug("Descriptor file for [" + objectName + "] is [" + retDescriptorFile + " ].");
return retDescriptorFile;
}
/**
* Retrieves all the discovery information for a War resources. We are retrieving all the information
* so that there is only ever one call to the MBeanServer to get the deployed mbeans, therefore saving
* some performance if it did this for each and every war resource one at a time.
*
* @param connection EmsConnection to get the mbean information
* @param jbossManMBeanNames Name of the main jboss.management mbeans for a collection of wars.
* @return map holds all the war deployment information for the objects passed in the objectNames collection
*/
public static Map<String, List<WarDeploymentInformation>> getWarDeploymentInformation(EmsConnection connection,
List<String> jbossManMBeanNames) {
// We need a list of informations, as one jsr77 deployment can end up in multiple web apps in different vhosts
HashMap<String, List<WarDeploymentInformation>> retDeploymentInformationMap = new HashMap<String, List<WarDeploymentInformation>>();
// will contain information on all deployments
Collection deploymentInfos;
try {
// NOTE: This is an expensive operation, since it returns a bunch of large objects.
deploymentInfos = getDeploymentInformations(connection);
}
catch (Exception e) {
return null;
}
String separator = System.getProperty("file.separator");
boolean isOnWin = separator.equals("\\");
// Loop through the deployment infos, and find the deployment infos corresponding to each of the
// jboss.management/JSR77 MBean names that were passed into this method. From the deployment infos,
// we can figure out the vhost(s) and context root for each WAR.
for (Object deploymentInfo : deploymentInfos) {
try {
// NOTE: There may be more than one jboss.web MBean,
// e.g. "jboss.web:J2EEApplication=none,J2EEServer=none,j2eeType=WebModule,name=//localhost/jmx-console",
// associated with a given WAR deployment, in which case, the "deployedObject" field will be
// arbitrarily set to the name of one of the jboss.web MBeans.
ObjectName jbossWebObjectName = getFieldValue(deploymentInfo, "deployedObject", ObjectName.class);
if (jbossWebObjectName != null) {
// e.g. "jmx-console.war"
String shortName = getFieldValue(deploymentInfo, "shortName", String.class);
for (String jbossManMBeanName : jbossManMBeanNames) {
ObjectName jbossManObjectName = new ObjectName(jbossManMBeanName);
String jbossManWarName = jbossManObjectName.getKeyProperty("name");
if (shortName.equals(jbossManWarName)) {
log.debug("Found DeploymentInfo for WAR " + shortName + ".");
// The only reliable way to determine the vhosts associated with the WAR is to use
// the "mbeans" field, whose value is a list of all the Servlet MBeans,
// .e.g. "jboss.web:J2EEApplication=none,J2EEServer=none,WebModule=//localhost/jmx-console,j2eeType=Servlet,name=default",
// corresponding to the WAR (one per servlet per vhost).
List servletObjectNames = getFieldValue(deploymentInfo, "mbeans", List.class);
Set<String> webModuleNames = new HashSet();
for (Object servletObjectName : servletObjectNames) {
// e.g. Figure out the web module name, e.g. "//localhost/jmx-console".
// NOTE: We must use reflection when working with the returned ObjectNames, since EMS
// loaded them using a different classloader. Attempting to access them directly
// would cause ClassCastExceptions.
Class<? extends Object> objectNameClass = servletObjectName.getClass();
Method getKeyPropertyMethod = objectNameClass.getMethod("getKeyProperty", String.class);
String webModuleName = (String)getKeyPropertyMethod.invoke(servletObjectName, "WebModule");
webModuleNames.add(webModuleName);
}
log.debug("Found " + webModuleNames.size() + " Web modules for WAR " + shortName + ": "
+ webModuleNames);
String path = getPath(isOnWin, deploymentInfo);
List<WarDeploymentInformation> infos = new ArrayList<WarDeploymentInformation>();
for (String webModuleName : webModuleNames) {
WebModule webModule = parseWebModuleName(webModuleName);
WarDeploymentInformation deploymentInformation = new WarDeploymentInformation();
deploymentInformation.setVHost(webModule.vhost);
deploymentInformation.setFileName(path);
deploymentInformation.setContextRoot(webModule.contextRoot);
String jbossWebMBeanName = String.format(JBOSS_WEB_MBEAN_NAME_TEMPLATE, webModuleName);
jbossWebObjectName = ObjectName.getInstance(jbossWebMBeanName);
jbossWebMBeanName = jbossWebObjectName.getCanonicalName();
deploymentInformation.setJbossWebModuleMBeanObjectName(jbossWebMBeanName);
infos.add(deploymentInformation);
}
retDeploymentInformationMap.put(jbossManMBeanName, infos);
}
}
}
} catch (Exception evalError) {
log.warn("Failed to determine if a deployment contains our MBean", evalError);
}
}
return retDeploymentInformationMap;
}
private static String getPath(boolean onWin, Object deploymentInfo) throws IOException {
String path;
String url = getFieldValue(deploymentInfo, "url", URL.class).toString();
if (url.startsWith("file:/")) {
if (onWin) {
path = url.substring(6);
// listDeployed() always delivers / as path separator, so we need to correct this.
File file = new File(path);
path = file.getCanonicalPath();
}
else
path = url.substring(5);
} else {
path = url;
}
return path;
}
private static WebModule parseWebModuleName(String name) {
WebModule webModule = new WebModule();
/*
* Lets find out the real context root. The one passed is //<vhost>/<context>
* If it ends in a slash, it's the root context (context root -> "/"). Otherwise, the
* context root will be everything after the last slash (e.g. "jmx-console").
* BUT: We need to be careful with slashes inside the context root, as jmx/console
* is a valid context root as well.
*/
if (name.startsWith("//")) {
// e.g. "//localhost/jmx-console"
name = name.substring(2);
int firstSlashIndex = name.indexOf("/");
if (firstSlashIndex > -1) {
webModule.vhost = name.substring(0, firstSlashIndex);
webModule.contextRoot = name.substring(firstSlashIndex);
}
// Chop off the leading slash for context roots other than "/".
if (webModule.contextRoot.length() > 1) {
webModule.contextRoot = webModule.contextRoot.substring(1);
}
}
else { // Fallback just in case
int lastSlashIndex = name.lastIndexOf("/");
if (lastSlashIndex > 0) {
int lastIndex = name.length() - 1;
name = (lastSlashIndex == lastIndex) ? "/" : name.substring(lastSlashIndex + 1);
}
}
return webModule;
}
/**
* Return the path where the passed objectnames are deployed
* @param connection
* @param fileNames The objectNames of the EAR files we are interested in
* @return a Map with objectname as key and path as value. This map may be empty on error.
*/
public static Map<String,String> getEarDeploymentPath(EmsConnection connection, List<String> fileNames) {
Collection deploymentInfos =null;
String separator = System.getProperty("file.separator");
boolean isOnWin = separator.equals("\\");
Map<String,String> results = new HashMap<String,String>(fileNames.size());
try {
// Get the list of deployed modules
deploymentInfos = getDeploymentInformations(connection);
for (Object sdi : deploymentInfos) {
String file = getFieldValue(sdi, "url", URL.class).toString();
// loop over the input, find the matchin entry and add to the results.
for (String earName : fileNames) {
if (!file.endsWith(earName))
continue;
if (file.startsWith("file:/")) {
if (isOnWin) {
file = file.substring(6);
// listDeployed() always delivers / as path separator, so we need to correct this.
File tmp = new File(file);
file = tmp.getCanonicalPath();
}
else
file = file.substring(5);
}
results.put(earName,file);
}
}
}
catch (Exception e) {
return new HashMap<String,String>();
}
return results;
}
public static boolean isDuplicateJndiName(EmsConnection connection, String mbeanType, String jndiName) {
if ((jndiName != null) && (mbeanType != null)) {
String name = mbeanType + ",name=" + jndiName;
EmsBean bean = connection.getBean(name);
if (bean == null) {
return false;
} else {
return bean.isRegistered();
}
}
return false;
}
public static String getJndiNameBinding(EmsBean bean) {
String jndiNameBinding = "";
if (bean != null) {
EmsAttribute attribute = bean.getAttribute("JNDIName");
attribute.refresh();
jndiNameBinding = String.valueOf(attribute.getValue());
}
return jndiNameBinding;
}
/**
* Invoke the listDeployedModules() operation of the MainDeployer to obtain all deployed modules along their
* DeploymentInfo structures. Falls back to listDeployed() if neessary (see #getListDeployedOperation ).
* @param connection A valid EmsConnection to the AS
* @return Collection of DeploymentInfo
* @throws Exception If the listDeployed() or listDeployedModuls() ops can not be found.
*/
private static Collection getDeploymentInformations(EmsConnection connection) throws Exception {
Collection deploymentInfos = null;
EmsOperation operation = null;
try {
operation = getListDeployedOperation(connection);
if (operation == null) {
throw new UnsupportedOperationException(
"This JBossAS instance is unsupported; its MainDeployer MBean doesn't have a listDeployedModules or listDeployed operation.");
}
deploymentInfos = (Collection) operation.invoke(new Object[0]);
} catch (RuntimeException re) {
// Make a last ditch effort in case the call to listDeployedModules() failed due to
// https://jira.jboss.org/jira/browse/JBAS-5983.
if (operation != null && operation.getName().equals(LIST_DEPLOYED_MODULES_OP_NAME)) {
EmsBean mainDeployerMBean = connection.getBean(MAIN_DEPLOYER);
operation = mainDeployerMBean.getOperation(LIST_DEPLOYED_OP_NAME);
try {
deploymentInfos = (Collection) operation.invoke(new Object[0]);
}
catch (RuntimeException re2) {
deploymentInfos = null;
}
}
if (deploymentInfos == null) {
log.warn("Cannot determine deployed modules - cause: " + re);
throw new Exception(re);
}
}
return deploymentInfos;
}
private static <T> T getFieldValue(Object target, String name, Class<T> T) {
if (target == null)
return null;
Field field;
T ret;
try {
field = target.getClass().getField(name);
ret = (T) field.get(target);
if (T == ObjectName.class && ret != null) {
ret = (T) new ObjectName(ret.toString());
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
}
return ret;
}
/**
* Retrieves the virtual host MBeans for the webapp with the specified context root.
* VHost MBeans have the pattern "jboss.web:host=*,path=/<ctx_root>,type=Manager".
*
* @param contextRoot the context root
* @param emsConnection valid connection to the remote MBeanServer
* @return the list of VHost MBeans for this webapp
*/
public static List<EmsBean> getVHostsFromLocalManager(String contextRoot, EmsConnection emsConnection) {
String contextPath = WarDiscoveryHelper.getContextPath(contextRoot);
String pattern = "jboss.web:host=%host%,path=" + contextPath + ",type=Manager";
ObjectNameQueryUtility queryUtil = new ObjectNameQueryUtility(pattern);
List<EmsBean> managerMBeans = emsConnection.queryBeans(queryUtil.getTranslatedQuery());
return managerMBeans;
}
/**
* Retrieves the virtual host MBeans for the webapp with the specified context root.
* VHost MBeans have the pattern "jboss.web:WebModule=//snert.home.pilhuhn.de/dist-vhost,service=ClusterManager".
*
* @param contextRoot the context root
* @param emsConnection valid connection to the remote MBeanServer
* @return the list of VHost MBeans for this webapp
*/
public static List<EmsBean> getVHostsFromClusterManager(String contextRoot, EmsConnection emsConnection) {
String contextPath = WarDiscoveryHelper.getContextPath(contextRoot);
//String webModule = "//" + contextInfo.vhost + contextPath;
String pattern = "jboss.web:service=ClusterManager,WebModule=%webModule%";
ObjectNameQueryUtility queryUtil = new ObjectNameQueryUtility(pattern);
List<EmsBean> clusterManagerMBeans = emsConnection.queryBeans(queryUtil.getTranslatedQuery());
return clusterManagerMBeans;
}
static class WebModule {
String vhost;
String contextRoot;
}
}