/* * 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.enterprise.server.xmlschema; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.HashMap; import java.util.Map; 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.JAXBElement; import javax.xml.bind.Unmarshaller; import javax.xml.bind.ValidationEvent; import javax.xml.bind.ValidationEventLocator; import javax.xml.bind.util.ValidationEventCollector; import javax.xml.transform.stream.StreamSource; 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.rhq.core.domain.plugin.ServerPlugin; import org.rhq.enterprise.server.xmlschema.generated.serverplugin.ServerPluginDescriptorType; /** * Utilities for server-side plugin descriptors. * * @author John Mazzitelli * @author Ian Springer */ public abstract class ServerPluginDescriptorUtil { private static final Log LOG = LogFactory.getLog(ServerPluginDescriptorUtil.class); // the path to the server plugin descriptor found in all server plugins private static final String PLUGIN_DESCRIPTOR_PATH = "META-INF/rhq-serverplugin.xml"; // the names of all XSD schema files mapped to the package names where their generated classes are private static final Map<String, String> PLUGIN_SCHEMA_PACKAGES; // a context path consisting of all the plugin descriptors' package names private static final String PLUGIN_CONTEXT_PATH; static { // maps all xsd files to their generated package names for all known server plugin types; // if a new plugin type is ever added, you must ensure you add the new plugin type's xsd/package here // See also: https://docs.jboss.org/author/display/RHQ/Design-Server+Side+Plugins#Design-ServerSidePlugins-xmlschemas PLUGIN_SCHEMA_PACKAGES = new HashMap<String, String>(); PLUGIN_SCHEMA_PACKAGES.put(XmlSchemas.XSD_SERVERPLUGIN, XmlSchemas.PKG_SERVERPLUGIN); PLUGIN_SCHEMA_PACKAGES.put(XmlSchemas.XSD_SERVERPLUGIN_GENERIC, XmlSchemas.PKG_SERVERPLUGIN_GENERIC); PLUGIN_SCHEMA_PACKAGES.put(XmlSchemas.XSD_SERVERPLUGIN_CONTENT, XmlSchemas.PKG_SERVERPLUGIN_CONTENT); PLUGIN_SCHEMA_PACKAGES.put(XmlSchemas.XSD_SERVERPLUGIN_ALERT, XmlSchemas.PKG_SERVERPLUGIN_ALERT); PLUGIN_SCHEMA_PACKAGES.put(XmlSchemas.XSD_SERVERPLUGIN_BUNDLE, XmlSchemas.PKG_SERVERPLUGIN_BUNDLE); PLUGIN_SCHEMA_PACKAGES.put(XmlSchemas.XSD_SERVERPLUGIN_PACKAGETYPE, XmlSchemas.PKG_SERVERPLUGIN_PACKAGETYPE); PLUGIN_SCHEMA_PACKAGES.put(XmlSchemas.XSD_SERVERPLUGIN_DRIFT, XmlSchemas.PKG_SERVERPLUGIN_DRIFT); // so we only have to do this once, build a ':' separated context path containing all schema package names StringBuilder packages = new StringBuilder(); for (String packageName : PLUGIN_SCHEMA_PACKAGES.values()) { packages.append(packageName).append(':'); } packages.setLength(packages.length() - 1); // delete the ending ':' so it isn't in our path PLUGIN_CONTEXT_PATH = packages.toString(); } /** * 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 or different types */ public static ServerPlugin determineObsoletePlugin(ServerPlugin plugin1, ServerPlugin plugin2) { if (!plugin1.getName().equals(plugin2.getName())) { throw new IllegalArgumentException("The two plugins don't have the same name:" + plugin1 + ":" + plugin2); } if (!plugin1.getType().equals(plugin2.getType())) { throw new IllegalArgumentException("The two plugins don't have the same type:" + 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, ServerPluginDescriptorType descriptor) throws Exception { if (descriptor == null) { descriptor = loadPluginDescriptorFromUrl(pluginFile.toURI().toURL()); if (descriptor == null) { throw new Exception("Plugin is missing a descriptor: " + pluginFile); } } 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); } } /** * Loads a plugin descriptor from the given plugin jar and returns it. If the given jar does not * have a server plugin descriptor, <code>null</code> will be returned, meaning this is not * a server plugin jar. * * @param pluginJarFileUrl URL to a plugin jar file * @return the plugin descriptor found in the given plugin jar file, or <code>null</code> if there * is no plugin descriptor in the jar file * @throws Exception if failed to parse the descriptor file found in the plugin jar */ public static ServerPluginDescriptorType loadPluginDescriptorFromUrl(URL pluginJarFileUrl) throws Exception { final Log logger = LogFactory.getLog(ServerPluginDescriptorUtil.class); if (pluginJarFileUrl == null) { throw new Exception("A valid plugin JAR URL must be supplied."); } if (logger.isDebugEnabled()) { logger.debug("Loading plugin descriptor from plugin jar at [" + pluginJarFileUrl + "]..."); } testPluginJarIsReadable(pluginJarFileUrl); JarInputStream jis = null; JarEntry descriptorEntry = null; 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(); } } ServerPluginDescriptorType pluginDescriptor = null; if (descriptorEntry != null) { Unmarshaller unmarshaller = null; try { unmarshaller = getServerPluginDescriptorUnmarshaller(); Object jaxbElement = unmarshaller.unmarshal(jis); pluginDescriptor = ((JAXBElement<? extends ServerPluginDescriptorType>) jaxbElement).getValue(); } finally { if (unmarshaller != null) { ValidationEventCollector validationEventCollector = (ValidationEventCollector)unmarshaller.getEventHandler(); logValidationEvents(pluginJarFileUrl, validationEventCollector); } } } return pluginDescriptor; } catch (Exception e) { throw new Exception("Could not successfully parse the plugin descriptor [" + PLUGIN_DESCRIPTOR_PATH + "] found in plugin jar at [" + pluginJarFileUrl + "]", e); } finally { if (jis != null) { try { jis.close(); } catch (Exception e) { logger.warn("Cannot close jar stream [" + pluginJarFileUrl + "]. Cause: " + e); } } } } /** * This will return a JAXB unmarshaller that will enable the caller to parse a server plugin * descriptor. The returned unmarshaller will have a {@link ValidationEventCollector} * installed as an {@link Unmarshaller#getEventHandler() event handler} which can be used * to obtain error messages if the unmarshaller fails to parse an XML document. * * @return a JAXB unmarshaller enabling the caller to parse server plugin descriptors * * @throws Exception if an unmarshaller could not be created */ public static Unmarshaller getServerPluginDescriptorUnmarshaller() throws Exception { // create the JAXB context with all the generated plugin packages in it JAXBContext jaxbContext; try { jaxbContext = JAXBContext.newInstance(PLUGIN_CONTEXT_PATH); } catch (Exception e) { throw new Exception("Failed to create JAXB Context.", e); } // create the unmarshaller that can be used to parse XML documents containing server plugin descriptors Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); // enable schema validation to ensure the XML documents parsed by the unmarshaller are valid descriptors ClassLoader cl = ServerPluginDescriptorUtil.class.getClassLoader(); SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); StreamSource[] schemaSources = new StreamSource[PLUGIN_SCHEMA_PACKAGES.size()]; int i = 0; for (String schemaPath : PLUGIN_SCHEMA_PACKAGES.keySet()) { URL schemaURL = cl.getResource(schemaPath); schemaSources[i++] = new StreamSource(schemaURL.toExternalForm()); } Schema pluginSchema = schemaFactory.newSchema(schemaSources); unmarshaller.setSchema(pluginSchema); ValidationEventCollector vec = new ValidationEventCollector(); unmarshaller.setEventHandler(vec); return unmarshaller; } private static void testPluginJarIsReadable(URL pluginJarFileUrl) throws Exception { InputStream inputStream = null; try { inputStream = pluginJarFileUrl.openStream(); } catch (IOException e) { throw new Exception("Unable to open plugin jar at [" + pluginJarFileUrl + "] for reading."); } finally { try { if (inputStream != null) { inputStream.close(); } } catch (IOException ignore) { } } } private static void logValidationEvents(URL pluginJarFileUrl, ValidationEventCollector validationEventCollector) { 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: LOG.warn(message); break; case ValidationEvent.FATAL_ERROR: LOG.error(message); break; } } } /** * 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 } } }