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 );
}
}