package org.dcache.gplazma.loader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXParseException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import java.io.Reader; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import org.dcache.gplazma.plugins.GPlazmaPlugin; /** * Class that parses the XML configuration file and returns useful * information. Plugin XML metadata has a root <tt>plugins</tt> element and * zero or more <tt>plugin</tt> child elements. Each <tt>plugin</tt> element * has metadata for that plugin as child elements of that <tt>plugin</tt> * element. * <p> * The following is a list of possible child elements (their local-name * values) along with the cardinality and a brief description. * <table> * <tr> * <th>local-name</th> * <th>cardinality</th> * <th>description</th> * </tr> * <tr> * <td>name</td> * <td>1..*</td> * <td>a name that may be used for this plugin. Names must be unique</td> * </tr> * <tr> * <td>class</td> * <td>1</td> * <td>the class name for this plugin (the result of * <code>Class.getName</code>)</td> * </tr> * <tr> * <td>default-control</td> * <td>0..1</td> * <td>currently unused</td> * </tr> * </table> * * The following is an example XML describing two plugins. One has a single * name, the other has two names. * * <pre> * <plugins> * <plugin> * <name>foo</name> * <class>org.dcache.foo.plugins.plugin1</class> * <default-control>required</default-control> * </plugin> * <plugin> * <name>bar</name> * <name>bar-o-matic</name> * <class>org.dcache.foo.plugins.plugin2</class> * <default-control>optional</default-control> * </plugin> * </plugins> * </pre> * * The parser will generate a set of PluginMetadata objects, one for each * valid plugin described in the XML. */ public class XmlParser { private static final Logger LOGGER = LoggerFactory.getLogger( XmlParser.class); private static final XPathFactory XPATH_FACTORY = XPathFactory.newInstance(); private static final String XPATH_EXPRESSION_PLUGINS_PLUGIN = "/plugins/plugin"; /** * An enumeration of allowed XML elements that are children of the * <tt>/plugins/plugin</tt> elements. The enumeration also holds the * local-name of the elements and allows easy access to that information. */ private enum XML_CHILD_NODE { NAME("name"), CLASS("class"), DEFAULT_CONTROL("default-control"); private static Map<String, XML_CHILD_NODE> LOCAL_NAME_STORE = new HashMap<>(); private String _localName; XML_CHILD_NODE( String localName) { _localName = localName; } public String getLocalName() { return _localName; } static { for( XML_CHILD_NODE node : XML_CHILD_NODE.values()) { LOCAL_NAME_STORE.put( node.getLocalName(), node); } } static boolean hasLocalName( String localName) { return LOCAL_NAME_STORE.containsKey( localName); } static XML_CHILD_NODE byLocalName( String localName) { return LOCAL_NAME_STORE.get( localName); } } private final InputSource _is; private final Set<PluginMetadata> _plugins = new HashSet<>(); /** * If the same class is used by multiple plugin descriptions then we * cannot know which plugin description is correct. Because of this, we * remove all plugin descriptions that use this class. */ private final Set<Class<? extends GPlazmaPlugin>> _bannedClasses = new HashSet<>(); public XmlParser( Reader source) { _is = new InputSource( source); } public void parse() { LOGGER.debug( "starting parse"); NodeList nodes = buildPluginNodeList(); LOGGER.debug( "NodeList has {} entries", nodes.getLength()); addPluginsFromNodeList( nodes); LOGGER.debug( "Created {} plugin metadata entries", _plugins.size()); } public Set<PluginMetadata> getPlugins() { return Collections.unmodifiableSet( _plugins); } private NodeList buildPluginNodeList() { XPath xpath = XPATH_FACTORY.newXPath(); XPathExpression expression; try { expression = xpath.compile( XPATH_EXPRESSION_PLUGINS_PLUGIN); } catch (XPathExpressionException e) { throw new RuntimeException( "Unable to compile XPath expression" + XPATH_EXPRESSION_PLUGINS_PLUGIN, e); } Object result; try { result = expression.evaluate( _is, XPathConstants.NODESET); } catch (XPathExpressionException e) { Throwable genericCause = e.getCause(); if( genericCause instanceof SAXParseException) { SAXParseException cause = (SAXParseException) genericCause; LOGGER.error( "Unable to parse plugin metadata: [{},{}] {}", cause.getLineNumber(), cause.getColumnNumber(), cause.getMessage()); } else { LOGGER.error( "Unable to parse plugin metadata: {}", genericCause.getMessage()); } return new EmptyNodeList(); } return (NodeList) result; } private void addPluginsFromNodeList( NodeList nodes) { for( int i = 0; i < nodes.getLength(); i++) { Node item = nodes.item( i); tryToAddPluginFromNode( item); } } private void tryToAddPluginFromNode( Node pluginRootNode) { try { PluginMetadata plugin = processPluginNode( pluginRootNode); LOGGER.debug( "Adding plugin {}", plugin.getPluginNames()); _plugins.add( plugin); } catch (IllegalArgumentException e) { LOGGER.error( "Unable register new plugin: {}", e.getMessage()); } } private PluginMetadata processPluginNode( Node pluginRootNode) { PluginMetadata metadata = new PluginMetadata(); addMetadataFromPluginNodeChildren( metadata, pluginRootNode); validMetadataGuard( metadata); return metadata; } private void validMetadataGuard( PluginMetadata metadata) { if( !metadata.isValid()) { throw new IllegalArgumentException( pluginName( metadata) + " metadata is incomplete"); } Class<? extends GPlazmaPlugin> thisPluginClass = metadata.getPluginClass(); LOGGER.debug( "examining plugin with class {}", thisPluginClass); if( removeIfClassAlreadyRegistered( thisPluginClass)) { _bannedClasses.add( thisPluginClass); throw new IllegalArgumentException( "Plugin '" + metadata.getShortestName() + "' uses class " + thisPluginClass.getName() + " which is already registered"); } if( _bannedClasses.contains( thisPluginClass)) { throw new IllegalArgumentException( "Plugin '" + metadata.getShortestName() + "' uses class " + thisPluginClass.getName() + " which was used by another plugin"); } } private boolean removeIfClassAlreadyRegistered( Class<? extends GPlazmaPlugin> pluginClass) { Iterator<PluginMetadata> itr = _plugins.iterator(); while (itr.hasNext()) { PluginMetadata registeredPlugin = itr.next(); Class<? extends GPlazmaPlugin> registeredPluginClass = registeredPlugin.getPluginClass(); LOGGER.debug("comparing plugin class {} against registered plugin with class {}", pluginClass, registeredPluginClass); if( pluginClass.equals( registeredPluginClass)) { itr.remove(); return true; } } return false; } private void addMetadataFromPluginNodeChildren( PluginMetadata metadata, Node pluginRootNode) { NodeList childNodes = pluginRootNode.getChildNodes(); for( int i = 0; i < childNodes.getLength(); i++) { Node childNode = childNodes.item( i); if( childNode.getNodeType() == Node.ELEMENT_NODE) { addMetadataFromChildNode( metadata, childNode); } } } private void addMetadataFromChildNode( PluginMetadata metadata, Node node) { String nodeName = node.getLocalName(); String value = node.getTextContent(); if( !XML_CHILD_NODE.hasLocalName( nodeName)) { LOGGER.warn( pluginName( metadata) + ": ignoring unknown field " + nodeName); return; } switch (XML_CHILD_NODE.byLocalName( nodeName)) { case NAME: metadata.addName( value); break; case CLASS: metadata.setPluginClass( value); break; case DEFAULT_CONTROL: metadata.setDefaultControl( value); break; } } private String pluginName( PluginMetadata metadata) { String name; if( metadata.hasPluginName()) { name = "Plugin " + metadata.getShortestName(); } else { name = "Plugin"; } return name; } /** * A trivial implementation of NodeList that is always empty. */ private static class EmptyNodeList implements NodeList { @Override public int getLength() { return 0; } @Override public Node item( int index) { return null; } } }