/* * Copyright 2007-2010 Sun Microsystems, Inc. * * This file is part of Project Darkstar Server. * * Project Darkstar Server 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 * distributed hereunder to you. * * Project Darkstar Server 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 for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * -- */ package com.sun.sgs.system; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.jar.Attributes; import java.util.jar.Attributes.Name; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.Manifest; /** A collection of extension jars used to determine ordering. */ class ExtJarGraph { // the location for an extension's properties static final String EXT_PROPERTIES_FILE = "META-INF/ext.properties"; // the property for defining any dependencies static final String DEPENDENCY_PROPERTY = "DEPENDS_ON"; // a map of all extension jars private final Map<String, JarNode> extNodes = new HashMap<String, JarNode>(); // a collection of jars that depend on other jars private final Set<JarNode> dependencyRoots = new HashSet<JarNode>(); // whether or not any preferences or dependencies are defined by the // the set of extension jars private boolean hasPreferences = false; private boolean hasDependencies = false; /** Creates an instance of {@code ExtJarGraph}. */ ExtJarGraph() { } /** * Adds a jar file to this collection. This method checks that the jar * includes a manifest with required details. It also looks for optional * properties and verifies that they are correctly formatted. * * @param jar the extension jar to add */ void addJarFile(JarFile jar) { JarNode node = null; // extract the name and version from the manifest, making sure that // the name is unique Manifest manifest = null; try { manifest = jar.getManifest(); } catch (IOException ioe) { throw new IllegalStateException("Failed to get manifest from " + "jar file: " + jar.getName()); } if (manifest == null) { throw new IllegalStateException("Manifest missing in extension " + "jar file: " + jar.getName()); } Attributes attrs = manifest.getMainAttributes(); String extName = attrs.getValue(Name.SPECIFICATION_TITLE); if (extName == null) { throw new IllegalStateException("Specification name required"); } if (extNodes.containsKey(extName)) { throw new IllegalStateException("Found two extensions with the " + "same name: " + extName); } String extVersion = attrs.getValue(Name.SPECIFICATION_VERSION); if (extVersion == null) { throw new IllegalStateException("Specification version required"); } // see if the jar file contains any darkstar properties JarEntry propertiesEntry = jar.getJarEntry(EXT_PROPERTIES_FILE); if (propertiesEntry != null) { Properties p = new Properties(); try { p.load(jar.getInputStream(propertiesEntry)); } catch (IOException ioe) { throw new IllegalStateException("Malformed properties in " + "ext jar: " + jar.getName()); } node = new JarNode(extName, extVersion, p); hasPreferences = true; String dependencies = (String) p.remove(DEPENDENCY_PROPERTY); if (dependencies != null) { hasDependencies = true; for (String dependency : dependencies.split(":")) { node.namedDependencies.add(dependency); } } } else { node = new JarNode(extName, extVersion); } extNodes.put(extName, node); dependencyRoots.add(node); } /** * Returns the name of a file containing all of the properties defined * in the collection of jar files, or null if no properties are defined. * This method will make sure that all dependencies are met, that there * are no circular dependenices, and that the properties are defined * correctly. * * @return a property filename or null * @throws IOException if there is a problem creating the property file */ String getPropertiesFile() throws IOException { if (extNodes.isEmpty() || (!hasPreferences)) { return null; } if (hasDependencies) { checkDependencies(); } // collect list properties in the right order Properties p = new Properties(); StringBuilder servicesLine = new StringBuilder(); StringBuilder managersLine = new StringBuilder(); StringBuilder nodeTypesLine = new StringBuilder(); StringBuilder authenticatorsLine = new StringBuilder(); StringBuilder profileListenersLine = new StringBuilder(); for (JarNode node : dependencyRoots) { buildProperties(node, p, servicesLine, managersLine, nodeTypesLine, authenticatorsLine, profileListenersLine); } if (servicesLine.length() != 0) { p.setProperty("com.sun.sgs.ext.services", servicesLine.toString()); } if (managersLine.length() != 0) { p.setProperty("com.sun.sgs.ext.managers", managersLine.toString()); } if (nodeTypesLine.length() != 0) { p.setProperty("com.sun.sgs.ext.services.node.types", nodeTypesLine.toString()); } if (authenticatorsLine.length() != 0) { p.setProperty("com.sun.sgs.ext.authenticators", authenticatorsLine.toString()); } if (profileListenersLine.length() != 0) { p.setProperty("com.sun.sgs.ext.kernel.profile.listeners", profileListenersLine.toString()); } // generate the properties file File propFile = File.createTempFile("extProperties", null); FileOutputStream out = new FileOutputStream(propFile); try { p.store(out, "Temporary extension properties file"); } finally { out.close(); } return propFile.getAbsolutePath(); } /** Check that all dependencies are met, and that there are no loops. */ private void checkDependencies() { // scan all the jar nodes checking that all depdencies are available for (JarNode node : extNodes.values()) { for (String dependency : node.namedDependencies) { JarNode dNode = extNodes.get(dependency); if (dNode == null) { throw new IllegalStateException("Missing dependency: " + dependency); } // if someone depends on dNode then it is removed from the // the root collection dependencyRoots.remove(dNode); node.dNodes.add(dNode); } } // see if there are any dependencies at all if (extNodes.size() == dependencyRoots.size()) { return; } // make sure there are no loops if (dependencyRoots.isEmpty()) { throw new IllegalStateException("Circular dependency not allowed"); } Set<String> names = new HashSet<String>(); for (JarNode node : dependencyRoots) { names.clear(); loopCheck(node, names); } } /** Recursively check that a given node doesn't lead to a loop. */ private static void loopCheck(JarNode node, Set<String> names) { if (names.contains(node.name)) { throw new IllegalStateException("Loop in dependent extensions: " + node.name); } names.add(node.name); for (JarNode dNode : node.dNodes) { loopCheck(dNode, new HashSet<String>(names)); } } /** Collects all properties and multi-element lines. */ private void buildProperties(JarNode node, Properties p, StringBuilder servicesLine, StringBuilder managersLine, StringBuilder nodeTypesLine, StringBuilder authenticatorsLine, StringBuilder profileListenersLine) { // gather properties from depdencies first for (JarNode dNode : node.dNodes) { buildProperties(dNode, p, servicesLine, managersLine, nodeTypesLine, authenticatorsLine, profileListenersLine); } // include this node's properties if they haven't already been included if (extNodes.remove(node.name) != null) { // remove the standard list properties to combine in a // separate collection Properties nodeProps = node.properties; String managers = (String) nodeProps.remove("com.sun.sgs.managers"); int managerCount = getElementCount(managers); String services = (String) nodeProps.remove("com.sun.sgs.services"); int serviceCount = getElementCount(services); String nodeTypes = (String) nodeProps.remove( "com.sun.sgs.services.node.types"); int nodeTypeCount = getElementCount(nodeTypes); // verify that the manager and service counts line up, or if // there are no managers then there is at most only one service if (managerCount != 0) { if (managerCount != serviceCount) { throw new IllegalStateException("Mis-matched Manager " + "and Service count for " + node.name); } } else { if (serviceCount > 1) { throw new IllegalStateException("Missing Managers for " + node.name); } } // verify that there are either no node types, or exactly the same // number as there are services if (nodeTypeCount != 0 && nodeTypeCount != serviceCount) { throw new IllegalStateException("Mis-matched Node Type " + "and Service count for " + node.name); } // if there are services then add them after figuring out how to // modify the manager and node types lines correctly if (serviceCount != 0) { if (managerCount == 0) { if (servicesLine.length() != 0) { // there are no new managers but there are previous // services so we need to pad a ":" to the line managersLine.append(":"); } } else { if ((servicesLine.length() != 0) && (managersLine.length() == 0)) { // there were previously services but no managers, so // pre-pend a ":" to the line addToLine(managersLine, ":" + managers); } else { // no padding is needed, just add the managers addToLine(managersLine, managers); } } if (nodeTypeCount == 0) { for (int i = 0; i < serviceCount; i++) { addToLine(nodeTypesLine, "ALL"); } } else { addToLine(nodeTypesLine, nodeTypes); } addToLine(servicesLine, services); } String authenticators = (String) nodeProps.remove("com.sun.sgs.app.authenticators"); if ((authenticators != null) && (authenticators.length() != 0)) { addToLine(authenticatorsLine, authenticators); } String profileListeners = (String) nodeProps.remove( "com.sun.sgs.impl.kernel.profile.listeners"); if ((profileListeners != null) && (profileListeners.length() != 0)) { addToLine(profileListenersLine, profileListeners); } // merge any remaining properties, failing if the same property // is assigned different values by separate extensions for (String key : nodeProps.stringPropertyNames()) { String value = (String) nodeProps.getProperty(key); Object oldValue = p.setProperty(key, value); if ((oldValue != null) && (!value.equals(oldValue))) { throw new IllegalStateException("Multiple values for " + "property: " + key); } } } } /** Count the number of colon-separated elements in the string. */ private int getElementCount(String str) { if ((str == null) || (str.length() == 0)) { return 0; } int count = 0; int pos = -1; do { count++; pos = str.indexOf(':', pos + 1); } while (pos != -1); return count; } /** Adds an element to a multi-element line. */ private static void addToLine(StringBuilder buf, String str) { if (buf.length() != 0) { buf.append(":" + str); } else { buf.append(str); } } /** Private class used to maintain state for a single extension jar. */ private static class JarNode { final String name; final String version; final Properties properties; final Set<String> namedDependencies = new HashSet<String>(); final Set<JarNode> dNodes = new HashSet<JarNode>(); JarNode(String name, String version) { this.name = name; this.version = version; this.properties = null; } JarNode(String name, String version, Properties properties) { this.name = name; this.version = version; this.properties = properties; } public boolean equals(Object o) { if (!(o instanceof JarNode)) { return false; } return name.equals(((JarNode) o).name); } public int hashCode() { return name.hashCode(); } } }