/*
* RHQ Management Platform
* Copyright (C) 2005-2011 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.core.clientapi.descriptor;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.ValidationEvent;
import javax.xml.bind.ValidationEventLocator;
import javax.xml.bind.util.ValidationEventCollector;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.maven.artifact.versioning.ComparableVersion;
import org.xml.sax.SAXException;
import org.rhq.core.clientapi.agent.PluginContainerException;
import org.rhq.core.clientapi.agent.metadata.PluginDependencyGraph;
import org.rhq.core.clientapi.agent.metadata.PluginDependencyGraph.PluginDependency;
import org.rhq.core.clientapi.descriptor.group.expressions.CannedGroupExpressions;
import org.rhq.core.clientapi.descriptor.plugin.Bundle;
import org.rhq.core.clientapi.descriptor.plugin.ParentResourceType;
import org.rhq.core.clientapi.descriptor.plugin.PlatformDescriptor;
import org.rhq.core.clientapi.descriptor.plugin.PluginDescriptor;
import org.rhq.core.clientapi.descriptor.plugin.ResourceDescriptor;
import org.rhq.core.clientapi.descriptor.plugin.RunsInsideType;
import org.rhq.core.clientapi.descriptor.plugin.ServerDescriptor;
import org.rhq.core.clientapi.descriptor.plugin.ServiceDescriptor;
import org.rhq.core.domain.plugin.Plugin;
import org.rhq.core.util.exception.WrappedRemotingException;
/**
* Utilities for agent plugin descriptors.
*
* @author John Mazzitelli
* @author Ian Springer
*/
public abstract class AgentPluginDescriptorUtil {
private static final Log LOG = LogFactory.getLog(AgentPluginDescriptorUtil.class);
private static final String PLUGIN_DESCRIPTOR_PATH = "META-INF/rhq-plugin.xml";
private static final String PLUGIN_SCHEMA_PATH = "rhq-plugin.xsd";
private static final String CANNED_GROUP_EXPRESSION_SCHEMA_PATH="rhq-canned-groups.xsd";
private static final String CANNED_GROUP_EXPRESSION_DESCRIPTOR_PATH="META-INF/rhq-group-expressions.xml";
/**
* Determines which of the two plugins is obsolete - in other words, this determines which
* plugin is older. Each plugin must have the same logical name, but
* one of which will be determined to be obsolete and should not be deployed.
* If they have the same MD5, they are identical, so <code>null</code> will be returned.
* Otherwise, the versions are compared and the one with the oldest version is obsolete.
* If they have the same versions, the one with the oldest timestamp is obsolete.
* If they have the same timestamp too, we have no other way to determine obsolescence so plugin1
* will be picked arbitrarily and a message will be logged when this occurs.
*
* @param plugin1
* @param plugin2
* @return a reference to the obsolete plugin (plugin1 or plugin2 reference will be returned)
* <code>null</code> is returned if they are the same (i.e. they have the same MD5)
* @throws IllegalArgumentException if the two plugins have different logical names
*/
public static Plugin determineObsoletePlugin(Plugin plugin1, Plugin plugin2) {
if (!plugin1.getName().equals(plugin2.getName())) {
throw new IllegalArgumentException("The two plugins don't have the same name:" + plugin1 + ":" + plugin2);
}
if (plugin1.getMd5().equals(plugin2.getMd5())) {
return null;
} else {
String version1Str = plugin1.getVersion();
String version2Str = plugin2.getVersion();
ComparableVersion plugin1Version = new ComparableVersion((version1Str != null) ? version1Str : "0");
ComparableVersion plugin2Version = new ComparableVersion((version2Str != null) ? version2Str : "0");
if (plugin1Version.equals(plugin2Version)) {
if (plugin1.getMtime() == plugin2.getMtime()) {
LOG.info("Plugins [" + plugin1 + ", " + plugin2
+ "] are the same logical plugin but have different content. The plugin [" + plugin1
+ "] will be considered obsolete.");
return plugin1;
} else if (plugin1.getMtime() < plugin2.getMtime()) {
return plugin1;
} else {
return plugin2;
}
} else if (plugin1Version.compareTo(plugin2Version) < 0) {
return plugin1;
} else {
return plugin2;
}
}
}
/**
* Returns the version for the plugin represented by the given descriptor/file.
* If the descriptor defines a version, that is considered the version of the plugin.
* However, if the plugin descriptor does not define a version, the plugin jar's manifest
* is searched for an implementation version string and if one is found that is the version
* of the plugin. If the manifest entry is also not found, the plugin does not have a version
* associated with it, which causes this method to throw an exception.
*
* @param pluginFile the plugin jar
* @param descriptor the plugin descriptor as found in the plugin jar (if <code>null</code>,
* the plugin file will be read and the descriptor parsed from it)
* @return the version of the plugin
* @throws Exception if the plugin is invalid, there is no version for the plugin or the version string is invalid
*/
public static ComparableVersion getPluginVersion(File pluginFile, PluginDescriptor descriptor) throws Exception {
if (descriptor == null) {
descriptor = loadPluginDescriptorFromUrl(pluginFile.toURI().toURL());
}
String version = descriptor.getVersion();
if (version == null) {
Manifest manifest = getManifest(pluginFile);
if (manifest != null) {
version = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);
}
}
if (version == null) {
throw new Exception("No version is defined for plugin jar [" + pluginFile
+ "]. A version must be defined either via the MANIFEST.MF [" + Attributes.Name.IMPLEMENTATION_VERSION
+ "] attribute or via the plugin descriptor 'version' attribute.");
}
try {
return new ComparableVersion(version);
} catch (RuntimeException e) {
throw new Exception("Version [" + version + "] for [" + pluginFile + "] did not parse", e);
}
}
/**
* Obtains the manifest of the plugin file represented by the given deployment info.
* Use this method rather than calling deploymentInfo.getManifest()
* (workaround for https://jira.jboss.org/jira/browse/JBAS-6266).
*
* @param pluginFile the plugin file
* @return the deployed plugin's manifest
*/
private static Manifest getManifest(File pluginFile) {
try {
JarFile jarFile = new JarFile(pluginFile);
try {
Manifest manifest = jarFile.getManifest();
return manifest;
} finally {
jarFile.close();
}
} catch (Exception ignored) {
return null; // this is OK, it just means we do not have a manifest
}
}
/**
* Given an existing dependency graph and a plugin descriptor, this will add that plugin and its dependencies
* to the dependency graph.
*
* @param dependencyGraph
* @param descriptor
*/
public static void addPluginToDependencyGraph(PluginDependencyGraph dependencyGraph, PluginDescriptor descriptor) {
String pluginName = descriptor.getName();
List<PluginDependencyGraph.PluginDependency> dependencies = new ArrayList<PluginDependencyGraph.PluginDependency>();
for (PluginDescriptor.Depends dependency : descriptor.getDepends()) {
String dependencyName = dependency.getPlugin();
boolean useClasses = dependency.isUseClasses(); // TODO this may not be used anymore
boolean required = true; // all <depends> plugins are implicitly required
dependencies.add(new PluginDependencyGraph.PluginDependency(dependencyName, useClasses, required));
}
List<PlatformDescriptor> platforms = descriptor.getPlatforms();
List<ServerDescriptor> servers = descriptor.getServers();
List<ServiceDescriptor> services = descriptor.getServices();
for (PlatformDescriptor platform : platforms) {
addOptionalDependency(platform, dependencies);
}
for (ServerDescriptor server : servers) {
addOptionalDependency(server, dependencies);
}
for (ServiceDescriptor service : services) {
addOptionalDependency(service, dependencies);
}
dependencyGraph.addPlugin(pluginName, dependencies);
return;
}
private static void addOptionalDependency(PlatformDescriptor platform,
List<PluginDependencyGraph.PluginDependency> dependencies) {
for (ServerDescriptor childServer : platform.getServers()) {
addOptionalDependency(childServer, dependencies);
}
for (ServiceDescriptor childService : platform.getServices()) {
addOptionalDependency(childService, dependencies);
}
addOptionalDependency(platform.getRunsInside(), dependencies);
addOptionalBundleDependency(platform, dependencies);
return;
}
private static void addOptionalDependency(ServerDescriptor server,
List<PluginDependencyGraph.PluginDependency> dependencies) {
for (ServerDescriptor childServer : server.getServers()) {
addOptionalDependency(childServer, dependencies);
}
for (ServiceDescriptor childService : server.getServices()) {
addOptionalDependency(childService, dependencies);
}
addOptionalDependency(server.getRunsInside(), dependencies);
addOptionalDependency(server.getSourcePlugin(), dependencies);
addOptionalBundleDependency(server, dependencies);
return;
}
private static void addOptionalDependency(ServiceDescriptor service,
List<PluginDependencyGraph.PluginDependency> dependencies) {
for (ServiceDescriptor childService : service.getServices()) {
addOptionalDependency(childService, dependencies);
}
addOptionalDependency(service.getRunsInside(), dependencies);
addOptionalDependency(service.getSourcePlugin(), dependencies);
addOptionalBundleDependency(service, dependencies);
}
private static void addOptionalDependency(RunsInsideType runsInside,
List<PluginDependencyGraph.PluginDependency> dependencies) {
if (runsInside != null) {
List<ParentResourceType> parents = runsInside.getParentResourceType();
for (ParentResourceType parent : parents) {
addOptionalDependency(parent.getPlugin(), dependencies);
}
}
return;
}
private static void addOptionalBundleDependency(ResourceDescriptor resource, List<PluginDependency> dependencies) {
if (resource.getBundle() != null && resource.getBundle().getTargets() != null) {
for (Bundle.Targets.ResourceType t : resource.getBundle().getTargets().getResourceType()) {
addOptionalDependency(t.getPlugin(), dependencies);
}
}
}
private static void addOptionalDependency(String pluginName,
List<PluginDependencyGraph.PluginDependency> dependencies) {
if (pluginName != null) {
boolean useClasses = false;
boolean required = false;
PluginDependency dep = new PluginDependencyGraph.PluginDependency(pluginName, useClasses, required);
if (!dependencies.contains(dep)) {
// only add it if it doesn't exist yet - this is so we don't override a required dep with an optional one
dependencies.add(dep);
}
}
return;
}
/**
* Retrieves file content as string from given jar
* @param pluginJarFileUrl URL to a plugin jar file
* @return content of additionPath file as String, or null if file does not exist in JAR
* @throws PluginContainerException if we fail to read content
*/
public static CannedGroupExpressions loadCannedExpressionsFromUrl(URL pluginJarFileUrl) throws PluginContainerException {
final Log logger = LogFactory.getLog(AgentPluginDescriptorUtil.class);
if (pluginJarFileUrl == null) {
throw new PluginContainerException("A valid plugin JAR URL must be supplied.");
}
logger.debug("Loading plugin additions from plugin jar at [" + pluginJarFileUrl + "]...");
ValidationEventCollector validationEventCollector = new ValidationEventCollector();
testPluginJarIsReadable(pluginJarFileUrl);
JarInputStream jis = null;
JarEntry descriptorEntry = null;
try {
jis = new JarInputStream(pluginJarFileUrl.openStream());
JarEntry nextEntry = jis.getNextJarEntry();
while (nextEntry != null && descriptorEntry == null) {
if (CANNED_GROUP_EXPRESSION_DESCRIPTOR_PATH.equals(nextEntry.getName())) {
descriptorEntry = nextEntry;
} else {
jis.closeEntry();
nextEntry = jis.getNextJarEntry();
}
}
if (descriptorEntry == null) {
logger.debug("Plugin additions not found");
// plugin additions are optional thing
return null;
}
return parseCannedGroupExpressionsDescriptor(jis, validationEventCollector);
} catch (Exception e) {
throw new PluginContainerException("Could not parse the plugin additions ["
+ CANNED_GROUP_EXPRESSION_DESCRIPTOR_PATH + "] found in plugin jar at [" + pluginJarFileUrl + "].",
new WrappedRemotingException(e));
} finally {
if (jis != null) {
try {
jis.close();
} catch (Exception e) {
logger.warn("Cannot close jar stream [" + pluginJarFileUrl + "]. Cause: " + e);
}
}
}
}
/**
* Loads a plugin descriptor from the given plugin jar and returns it.
*
* This is a static method to provide a convenience method for others to be able to use.
*
* @param pluginJarFileUrl URL to a plugin jar file
* @return the plugin descriptor found in the given plugin jar file
* @throws PluginContainerException if failed to find or parse a descriptor file in the plugin jar
*/
public static PluginDescriptor loadPluginDescriptorFromUrl(URL pluginJarFileUrl) throws PluginContainerException {
final Log logger = LogFactory.getLog(AgentPluginDescriptorUtil.class);
if (pluginJarFileUrl == null) {
throw new PluginContainerException("A valid plugin JAR URL must be supplied.");
}
logger.debug("Loading plugin descriptor from plugin jar at [" + pluginJarFileUrl + "]...");
testPluginJarIsReadable(pluginJarFileUrl);
JarInputStream jis = null;
JarEntry descriptorEntry = null;
ValidationEventCollector validationEventCollector = new ValidationEventCollector();
try {
jis = new JarInputStream(pluginJarFileUrl.openStream());
JarEntry nextEntry = jis.getNextJarEntry();
while (nextEntry != null && descriptorEntry == null) {
if (PLUGIN_DESCRIPTOR_PATH.equals(nextEntry.getName())) {
descriptorEntry = nextEntry;
} else {
jis.closeEntry();
nextEntry = jis.getNextJarEntry();
}
}
if (descriptorEntry == null) {
throw new Exception("The plugin descriptor does not exist");
}
return parsePluginDescriptor(jis, validationEventCollector);
} catch (Exception e) {
throw new PluginContainerException("Could not successfully parse the plugin descriptor ["
+ PLUGIN_DESCRIPTOR_PATH + "] found in plugin jar at [" + pluginJarFileUrl + "].",
new WrappedRemotingException(e));
} finally {
if (jis != null) {
try {
jis.close();
} catch (Exception e) {
logger.warn("Cannot close jar stream [" + pluginJarFileUrl + "]. Cause: " + e);
}
}
logValidationEvents(pluginJarFileUrl, validationEventCollector, logger);
}
}
/**
* Parses a descriptor from InputStream without a validator.
* @param is input to check
* @return parsed PluginDescriptor
* @throws PluginContainerException if validation fails
*/
public static PluginDescriptor parsePluginDescriptor(InputStream is) throws PluginContainerException {
return parsePluginDescriptor(is, new ValidationEventCollector());
}
/**
* Parses a descriptor from InputStream without a validator.
* @param is input to check
* @return parsed PluginDescriptor
* @throws PluginContainerException if validation fails
*/
public static PluginDescriptor parsePluginDescriptor(InputStream is,
ValidationEventCollector validationEventCollector) throws PluginContainerException {
JAXBContext jaxbContext;
return (PluginDescriptor) parsePluginDescriptor(is, validationEventCollector, PLUGIN_SCHEMA_PATH, DescriptorPackages.PC_PLUGIN);
}
/**
* Parses a descriptor from InputStream without a validator.
* @param is input to check
* @return parsed PluginDescriptor
* @throws PluginContainerException if validation fails
*/
public static CannedGroupExpressions parseCannedGroupExpressionsDescriptor(InputStream is,
ValidationEventCollector validationEventCollector) throws PluginContainerException {
JAXBContext jaxbContext;
return (CannedGroupExpressions) parsePluginDescriptor(is, validationEventCollector, CANNED_GROUP_EXPRESSION_SCHEMA_PATH, DescriptorPackages.CANNED_EXPRESSIONS);
}
/**
* Parses a descriptor from InputStream without a validator.
* @param is input to check
* @return parsed PluginDescriptor
* @throws PluginContainerException if validation fails
*/
private static Object parsePluginDescriptor(InputStream is,
ValidationEventCollector validationEventCollector, String xsd, String jaxbPackage) throws PluginContainerException {
JAXBContext jaxbContext;
try {
jaxbContext = JAXBContext.newInstance(jaxbPackage);
} catch (Exception e) {
throw new PluginContainerException("Failed to create JAXB Context.", new WrappedRemotingException(e));
}
Unmarshaller unmarshaller;
try {
unmarshaller = jaxbContext.createUnmarshaller();
// Enable schema validation
URL pluginSchemaURL = AgentPluginDescriptorUtil.class.getClassLoader().getResource(xsd);
Schema pluginSchema = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(
pluginSchemaURL);
unmarshaller.setSchema(pluginSchema);
unmarshaller.setEventHandler(validationEventCollector);
return unmarshaller.unmarshal(is);
} catch (JAXBException e) {
throw new PluginContainerException(e);
} catch (SAXException e) {
throw new PluginContainerException(e);
}
}
private static void logValidationEvents(URL pluginJarFileUrl, ValidationEventCollector validationEventCollector,
Log logger) {
for (ValidationEvent event : validationEventCollector.getEvents()) {
// First build the message to be logged. The message will look something like this:
//
// Validation fatal error while parsing [jopr-jboss-as-plugin-4.3.0-SNAPSHOT.jar:META-INF/rhq-plugin.xml]
// at line 221, column 94: cvc-minInclusive-valid: Value '20000' is not facet-valid with respect to
// minInclusive '30000' for type '#AnonType_defaultIntervalmetric'.
//
StringBuilder message = new StringBuilder();
String severity = null;
switch(event.getSeverity()) {
case ValidationEvent.WARNING:
severity = "warning";
break;
case ValidationEvent.ERROR:
severity = "error";
break;
case ValidationEvent.FATAL_ERROR:
severity = "fatal error";
break;
}
message.append("Validation ").append(severity);
File pluginJarFile = new File(pluginJarFileUrl.getPath());
message.append(" while parsing [").append(pluginJarFile.getName()).append(":").append(PLUGIN_DESCRIPTOR_PATH).append("]");
ValidationEventLocator locator = event.getLocator();
message.append(" at line ").append(locator.getLineNumber());
message.append(", column ").append(locator.getColumnNumber());
message.append(": ").append(event.getMessage());
// Now write the message to the log at an appropriate level.
switch(event.getSeverity()) {
case ValidationEvent.WARNING:
case ValidationEvent.ERROR:
logger.warn(message);
break;
case ValidationEvent.FATAL_ERROR:
logger.error(message);
break;
}
}
}
private static void testPluginJarIsReadable(URL pluginJarFileUrl) throws PluginContainerException {
InputStream inputStream = null;
try {
inputStream = pluginJarFileUrl.openStream();
} catch (IOException e) {
throw new PluginContainerException("Unable to open plugin jar at [" + pluginJarFileUrl + "] for reading.");
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException ignore) {
}
}
}
}