package org.ops4j.pax.construct.inherit; /* * Copyright 2007 Stuart McCulloch * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.io.File; import java.io.IOException; import java.io.Reader; import java.io.Writer; import org.codehaus.plexus.util.IOUtil; import org.codehaus.plexus.util.xml.Xpp3Dom; import org.codehaus.plexus.util.xml.Xpp3DomBuilder; import org.codehaus.plexus.util.xml.pull.MXParser; import org.codehaus.plexus.util.xml.pull.XmlPullParser; import org.codehaus.plexus.util.xml.pull.XmlPullParserException; import org.codehaus.plexus.util.xml.pull.XmlSerializer; /** * Plugin metadata, usually stored in META-INF/maven/plugin.xml */ public class PluginXml { private static final String EXECUTE_GOAL = "executeGoal"; private static final String EXECUTE_PHASE = "executePhase"; private final File m_file; private Xpp3Dom m_xml; /** * Parses a file presumed to contain plugin metadata * * @param file file containing plugin metadata * @throws XmlPullParserException * @throws IOException */ public PluginXml( File file ) throws XmlPullParserException, IOException { m_file = file; XmlPullParser parser = new MXParser(); Reader reader = StreamFactory.newXmlReader( m_file ); parser.setInput( reader ); m_xml = Xpp3DomBuilder.build( parser, false ); IOUtil.close( reader ); } /** * {@inheritDoc} */ public String toString() { return m_xml.getChild( "artifactId" ).getValue(); } /** * Gets all mojo descriptors * * @return an array of mojo descriptors */ public Xpp3Dom[] getMojos() { return m_xml.getChild( "mojos" ).getChildren( "mojo" ); } /** * Finds a mojo descriptor with a named goal * * @param goal mojo goal * @return first matching mojo descriptor, null if not found */ public Xpp3Dom findMojo( String goal ) { Xpp3Dom[] mojos = getMojos(); for( int i = 0; i < mojos.length; i++ ) { if( goal.equals( mojos[i].getChild( "goal" ).getValue() ) ) { return mojos[i]; } } return null; } /** * Merges a mojo descriptor with its super-mojo (inherit from it) * * The local mojo is considered the dominant one - if any parameters, configuration or requirements match then the * local version is kept. Any parameters, configuration or requirements not in the local mojo are appended to its * descriptor * * @param mojo local mojo inheriting from superMojo * @param superMojo mojo being extended */ public static void mergeMojo( Xpp3Dom mojo, Xpp3Dom superMojo ) { // clone superMojo to get an editable copy Xpp3Dom tempMojo = new Xpp3Dom( superMojo ); // duplicates are removed from the temp copy of the superMojo removeDuplicates( mojo, tempMojo, "parameters", "name/", true ); removeDuplicates( mojo, tempMojo, "configuration", null, false ); removeDuplicates( mojo, tempMojo, "requirements", "field-name/", true ); removeDuplicateExecuteTags( mojo, tempMojo ); // now we can safely append these sections setAppendMode( mojo.getChild( "parameters" ) ); setAppendMode( mojo.getChild( "configuration" ) ); setAppendMode( mojo.getChild( "requirements" ) ); Xpp3Dom.mergeXpp3Dom( mojo, tempMojo ); } /** * Removes any common elements from a given list in the temporary mojo * * Elements are matched by comparing their ids, which are located using a simple XML path notation. If the path ends * in '/' then the content of the id element is used, otherwise the name of the id element itself is used * * @param mojo dominant mojo * @param tempMojo temporary mojo * @param name name of the element list to check * @param path simple XML path with the location of the id element * @param verbose print warnings about any matching elements */ private static void removeDuplicates( Xpp3Dom mojo, Xpp3Dom tempMojo, String name, String path, boolean verbose ) { Xpp3Dom list = mojo.getChild( name ); Xpp3Dom tempList = tempMojo.getChild( name ); if( null == tempList || null == list ) { return; } for( int s = 0; s < tempList.getChildCount(); s++ ) { Xpp3Dom idElement = getIdElement( tempList.getChild( s ), path ); if( hasMatchingElement( list, path, verbose, idElement ) ) { // decrement index to avoid skipping entries, as list shrinks by one tempList.removeChild( s-- ); } } } /** * Searches an element list for any elements with the same id * * Elements are matched by comparing their ids, which are located using a simple XML path notation. If the path is * not null and ends in / then the content of the id element is used, otherwise the name of the id element is used * * @param list element list * @param path simple XML path with the location of the id element * @param verbose print warnings about any matching elements * @param theIdElement id element to compare against * @return true if element list contains a matching element */ private static boolean hasMatchingElement( Xpp3Dom list, String path, boolean verbose, Xpp3Dom theIdElement ) { for( int n = 0; n < list.getChildCount(); n++ ) { Xpp3Dom idElement = getIdElement( list.getChild( n ), path ); String lhs; String rhs; if( null != path && path.endsWith( "/" ) ) { lhs = theIdElement.getValue(); rhs = idElement.getValue(); } else { lhs = theIdElement.getName(); rhs = idElement.getName(); } if( lhs.equals( rhs ) ) { if( verbose ) { System.out.println( "[WARN] overriding field " + lhs ); } return true; } } return false; } /** * Finds the element that represents the id, according to an XML path * * @param element start element * @param path simple XML path with the location of the id element * @return id element */ private static Xpp3Dom getIdElement( Xpp3Dom element, String path ) { Xpp3Dom idElement = element; if( null != path ) { String[] idSegments = path.split( "/" ); for( int i = 0; i < idSegments.length; i++ ) { idElement = element.getChild( idSegments[i] ); } } return idElement; } /** * Special check for duplicate execute tags, as there are two discrete types (goal vs phase) * * @param mojo dominant mojo * @param tempMojo temporary mojo */ private static void removeDuplicateExecuteTags( Xpp3Dom mojo, Xpp3Dom tempMojo ) { Xpp3Dom executeNode = mojo.getChild( EXECUTE_PHASE ); if( null == executeNode ) { executeNode = mojo.getChild( EXECUTE_GOAL ); } if( executeNode != null ) { removeChild( tempMojo, tempMojo.getChild( EXECUTE_PHASE ) ); removeChild( tempMojo, tempMojo.getChild( EXECUTE_GOAL ) ); if( "none".equals( executeNode.getValue() ) ) { // allow removal of execute tags removeChild( mojo, executeNode ); } } } /** * Remove the child from the XML fragment * * @param xml xml fragment * @param child child node * @return true if node was removed, otherwise false */ private static boolean removeChild( Xpp3Dom xml, Xpp3Dom child ) { for( int i = xml.getChildCount() - 1; i >= 0; i-- ) { // reference equality test is ok if( xml.getChild( i ) == child ) { xml.removeChild( i ); return true; } } return false; } /** * Requests that all child elements are appended when the metadata is merged * * @param element an element in the plugin metadata */ private static void setAppendMode( Xpp3Dom element ) { if( null != element ) { element.setAttribute( Xpp3Dom.CHILDREN_COMBINATION_MODE_ATTRIBUTE, Xpp3Dom.CHILDREN_COMBINATION_APPEND ); } } /** * Writes the plugin metadata back to the file where it was loaded from * * @throws IOException */ public void write() throws IOException { String encoding = StreamFactory.getXmlEncoding( m_file ); Writer writer = StreamFactory.newXmlWriter( m_file ); XmlSerializer serializer = new PluginSerializer(); serializer.setOutput( writer ); serializer.startDocument( encoding, null ); m_xml.writeToSerializer( null, serializer ); serializer.endDocument(); IOUtil.close( writer ); } }