package org.ops4j.pax.construct.util; /* * Copyright 2007 Stuart McCulloch, Alin Dreghiciu * * 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 java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Properties; import org.apache.maven.model.Dependency; import org.apache.maven.model.Model; import org.apache.maven.model.Repository; import org.apache.maven.project.MavenProject; 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.XmlPullParser; import org.codehaus.plexus.util.xml.pull.XmlPullParserException; import org.codehaus.plexus.util.xml.pull.XmlSerializer; import org.ops4j.pax.construct.util.PomUtils.ExistingElementException; import org.ops4j.pax.construct.util.PomUtils.Pom; /** * Support round-trip editing of Maven POMs, preserving comments and formatting as much as possible */ public class XppPom implements Pom { /** * Underlying XML file */ private final File m_file; /** * Current XML document */ private Xpp3Dom m_pom; /** * Read Maven project details from existing file * * @param pomFile XML file containing Maven project model * @throws IOException */ public XppPom( File pomFile ) throws IOException { // protect against changes in working directory m_file = DirUtils.resolveFile( pomFile, true ); try { XmlPullParser parser = RoundTripXml.createParser(); Reader reader = StreamFactory.newXmlReader( m_file ); parser.setInput( reader ); m_pom = Xpp3DomBuilder.build( parser, false ); IOUtil.close( reader ); } catch( XmlPullParserException e ) { throw new IOException( e.getLocalizedMessage() ); } } /** * Create blank Maven project module * * @param pomFile XML file, may or may not exist * @param groupId project group id * @param artifactId project artifact id */ public XppPom( File pomFile, String groupId, String artifactId ) { // protect against changes in working directory m_file = DirUtils.resolveFile( pomFile, true ); m_pom = new Xpp3Dom( "project" ); // standard header cruft m_pom.setAttribute( "xmlns", "http://maven.apache.org/POM/4.0.0" ); m_pom.setAttribute( "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance" ); m_pom.setAttribute( "xsi:schemaLocation", "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd" ); Xpp3DomMap.putValue( m_pom, "modelVersion", "4.0.0" ); Xpp3DomMap.putValue( m_pom, "groupId", groupId ); Xpp3DomMap.putValue( m_pom, "artifactId", artifactId ); Xpp3DomMap.putValue( m_pom, "name", "" ); Xpp3DomMap.putValue( m_pom, "packaging", "pom" ); m_file.getParentFile().mkdirs(); } /** * {@inheritDoc} */ public String getId() { // follow the Maven standard... return getGroupId() + ':' + getArtifactId() + ':' + getPackaging() + ':' + getVersion(); } /** * {@inheritDoc} */ public String getParentId() { Xpp3Dom parent = m_pom.getChild( "parent" ); if( null == parent ) { return null; } Xpp3Dom groupId = parent.getChild( "groupId" ); Xpp3Dom artifactId = parent.getChild( "artifactId" ); Xpp3Dom version = parent.getChild( "version" ); // assume that the parent has pom packaging (seems reasonable assumption) return groupId.getValue() + ':' + artifactId.getValue() + ":pom:" + version.getValue(); } /** * {@inheritDoc} */ public String getGroupId() { Xpp3Dom groupId = m_pom.getChild( "groupId" ); Xpp3Dom parent = m_pom.getChild( "parent" ); if( null == groupId && null != parent ) { // inherit group from parent element groupId = parent.getChild( "groupId" ); } if( null == groupId ) { return null; } return groupId.getValue(); } /** * {@inheritDoc} */ public String getArtifactId() { return m_pom.getChild( "artifactId" ).getValue(); } /** * {@inheritDoc} */ public String getVersion() { Xpp3Dom version = m_pom.getChild( "version" ); Xpp3Dom parent = m_pom.getChild( "parent" ); if( null == version && null != parent ) { // inherit version from parent element version = parent.getChild( "version" ); } if( null == version ) { return null; } return version.getValue(); } /** * {@inheritDoc} */ public String getPackaging() { Xpp3Dom packaging = m_pom.getChild( "packaging" ); if( null == packaging ) { return "jar"; } return packaging.getValue(); } /** * {@inheritDoc} */ public List getModuleNames() { List names = new ArrayList(); Xpp3Dom modules = m_pom.getChild( "modules" ); if( null != modules ) { Xpp3Dom[] values = modules.getChildren( "module" ); for( int i = 0; i < values.length; i++ ) { names.add( values[i].getValue() ); } } return names; } /** * {@inheritDoc} */ public Pom getContainingPom() { try { File baseDir = getBasedir(); // check it really does contain our current project Pom pom = PomUtils.readPom( baseDir.getParentFile() ); if( pom.getModuleNames().contains( baseDir.getName() ) ) { return pom; } return null; } catch( IOException e ) { return null; } } /** * {@inheritDoc} */ public Pom getModulePom( String name ) { try { // check it really is a valid module if( getModuleNames().contains( name ) ) { return PomUtils.readPom( new File( m_file.getParentFile(), name ) ); } return null; } catch( IOException e ) { return null; } } /** * {@inheritDoc} */ public File getFile() { return m_file; } /** * {@inheritDoc} */ public File getBasedir() { return m_file.getParentFile(); } /** * {@inheritDoc} */ public boolean isBundleProject() { // local project, so can use very simple test based on packaging type return getPackaging().indexOf( "bundle" ) >= 0; } /** * {@inheritDoc} */ public String getBundleSymbolicName() { Xpp3Dom properties = m_pom.getChild( "properties" ); if( null != properties ) { Xpp3Dom symbolicName = properties.getChild( "bundle.symbolicName" ); if( null != symbolicName ) { return symbolicName.getValue(); } } return null; } /** * {@inheritDoc} */ public void setParent( Pom pom, String relativePath, boolean overwrite ) throws ExistingElementException { MavenProject project = new MavenProject( new Model() ); project.setGroupId( pom.getGroupId() ); project.setArtifactId( pom.getArtifactId() ); project.setVersion( pom.getVersion() ); setParent( project, relativePath, overwrite ); } /** * {@inheritDoc} */ public void setParent( MavenProject project, String relativePath, boolean overwrite ) throws ExistingElementException { if( m_pom.getChild( "parent" ) != null && !overwrite ) { throw new ExistingElementException( "parent" ); } Xpp3DomMap parent = new Xpp3DomMap( "parent" ); parent.putValue( "relativePath", relativePath ); parent.putValue( "groupId", project.getGroupId() ); parent.putValue( "artifactId", project.getArtifactId() ); parent.putValue( "version", project.getVersion() ); Xpp3Dom newPom = new Xpp3Dom( "project" ); newPom.addChild( parent ); m_pom = Xpp3DomHelper.mergeXpp3Dom( newPom, m_pom ); } /** * {@inheritDoc} */ public void setGroupId( String newGroupId ) { Xpp3Dom groupId = m_pom.getChild( "groupId" ); if( null == groupId ) { groupId = new Xpp3Dom( "groupId" ); m_pom.addChild( groupId ); } groupId.setValue( newGroupId ); } /** * {@inheritDoc} */ public void setVersion( String newVersion ) { Xpp3Dom version = m_pom.getChild( "version" ); if( null == version ) { version = new Xpp3Dom( "version" ); m_pom.addChild( version ); } version.setValue( newVersion ); } /** * {@inheritDoc} */ public void addRepository( Repository repository, boolean snapshots, boolean releases, boolean overwrite, boolean pluginRepo ) throws ExistingElementException { final String listName; final String elemName; if( pluginRepo ) { listName = "pluginRepositories"; elemName = "pluginRepository"; } else { listName = "repositories"; elemName = "repository"; } String id = repository.getId(); String url = repository.getUrl(); String xpath = listName + '/' + elemName + "[id='" + id + "' or url='" + url + "']"; // clear old elements when overwriting if( findChildren( xpath, overwrite ) && !overwrite ) { throw new ExistingElementException( elemName ); } Xpp3DomMap repo = new Xpp3DomMap( elemName ); repo.putValue( "id", id ); repo.putValue( "url", url ); if( !snapshots ) { Xpp3DomMap snapshotFlag = new Xpp3DomMap( "snapshots" ); snapshotFlag.putValue( "enabled", "false" ); repo.addChild( snapshotFlag ); } if( !releases ) { Xpp3DomMap releaseFlag = new Xpp3DomMap( "releases" ); releaseFlag.putValue( "enabled", "false" ); repo.addChild( releaseFlag ); } Xpp3Dom list = new Xpp3DomList( listName ); list.addChild( repo ); Xpp3Dom newPom = new Xpp3Dom( "project" ); newPom.addChild( list ); Xpp3DomHelper.mergeXpp3Dom( m_pom, newPom ); } /** * {@inheritDoc} */ public void addModule( String module, boolean overwrite ) throws ExistingElementException { String xpath = "modules/module[.='" + module + "']"; // clear old elements when overwriting if( findChildren( xpath, overwrite ) && !overwrite ) { throw new ExistingElementException( "module" ); } Xpp3Dom mod = new Xpp3Dom( "module" ); mod.setValue( module ); Xpp3Dom list = new Xpp3DomList( "modules" ); list.addChild( mod ); Xpp3Dom newPom = new Xpp3Dom( "project" ); newPom.addChild( list ); Xpp3DomHelper.mergeXpp3Dom( m_pom, newPom ); } /** * {@inheritDoc} */ public boolean removeModule( String module ) { String xpath = "modules/module[.='" + module + "']"; return findChildren( xpath, true ); } /** * {@inheritDoc} */ public void addDependency( Dependency dependency, boolean overwrite ) throws ExistingElementException { String groupId = dependency.getGroupId(); String artifactId = dependency.getArtifactId(); String xpath = "dependencies/dependency[groupId='" + groupId + "' and artifactId='" + artifactId + "']"; // clear old elements when overwriting if( findChildren( xpath, overwrite ) && !overwrite ) { throw new ExistingElementException( "dependency" ); } Xpp3DomMap dep = new Xpp3DomMap( "dependency" ); dep.putValue( "groupId", groupId ); dep.putValue( "artifactId", artifactId ); dep.putValue( "version", dependency.getVersion() ); dep.putValue( "scope", dependency.getScope() ); String type = dependency.getType(); if( !"jar".equals( type ) ) { // jar is the default type dep.putValue( "type", type ); } if( dependency.isOptional() ) { dep.putValue( "optional", "true" ); } Xpp3Dom list = new Xpp3DomList( "dependencies" ); list.addChild( dep ); Xpp3Dom newPom = new Xpp3Dom( "project" ); newPom.addChild( list ); Xpp3DomHelper.mergeXpp3Dom( m_pom, newPom ); } /** * {@inheritDoc} */ public boolean updateDependencyGroup( Dependency dependency, String newGroupId ) { String groupId = dependency.getGroupId(); String artifactId = dependency.getArtifactId(); boolean updated = false; String xpath1 = "dependencies/dependency[groupId='" + groupId + "' and artifactId='" + artifactId + "']"; updated = updateGroupId( xpath1, newGroupId ) || updated; String xpath2 = "dependencyManagement/" + xpath1; updated = updateGroupId( xpath2, newGroupId ) || updated; return updated; } /** * @param xpath simple XPATH query * @param newGroupId new group id * @return true if any elements were updated, otherwise false */ private boolean updateGroupId( String xpath, String newGroupId ) { XppPathQuery pathQuery = new XppPathQuery( xpath ); Xpp3Dom parent = pathQuery.queryParent( m_pom ); if( null == parent ) { return false; } int[] children = pathQuery.queryChildren( parent ); for( int i = 0; i < children.length; i++ ) { Xpp3Dom group = parent.getChild( children[i] ).getChild( "groupId" ); if( null != group ) { group.setValue( newGroupId ); } } return children.length > 0; } /** * @param xpath simple XPATH query * @param newVersion new version * @return true if any elements were updated, otherwise false */ private boolean updateVersion( String xpath, String newVersion ) { XppPathQuery pathQuery = new XppPathQuery( xpath ); Xpp3Dom parent = pathQuery.queryParent( m_pom ); if( null == parent ) { return false; } int[] children = pathQuery.queryChildren( parent ); for( int i = 0; i < children.length; i++ ) { Xpp3Dom fragment = parent.getChild( children[i] ); Xpp3Dom version = fragment.getChild( "version" ); if( null == version ) { version = new Xpp3Dom( "version" ); Xpp3DomList.addChild( fragment, 2, version ); } version.setValue( newVersion ); } return children.length > 0; } /** * {@inheritDoc} */ public boolean removeDependency( Dependency dependency ) { String groupId = dependency.getGroupId(); String artifactId = dependency.getArtifactId(); boolean updated = false; String xpath1 = "dependencies/dependency[groupId='" + groupId + "' and artifactId='" + artifactId + "']"; updated = findChildren( xpath1, true ) || updated; String xpath2 = "dependencyManagement/" + xpath1; updated = findChildren( xpath2, true ) || updated; return updated; } /** * {@inheritDoc} */ public void addExclusion( String groupId, String artifactId, boolean overwrite ) throws ExistingElementException { Xpp3Dom dependencies = m_pom.getChild( "dependencies" ); if( null == dependencies || dependencies.getChildCount() <= 0 ) { return; // can't exclude what isn't there! } String exclusionPath = "dependencies/dependency/exclusions/exclusion"; String xpath = exclusionPath + "[groupId='" + groupId + "' and artifactId='" + artifactId + "']"; // clear old elements when overwriting if( findChildren( xpath, overwrite ) && !overwrite ) { throw new ExistingElementException( "exclusion" ); } Xpp3DomMap exclude = new Xpp3DomMap( "exclusion" ); exclude.putValue( "groupId", groupId ); exclude.putValue( "artifactId", artifactId ); Xpp3Dom list = new Xpp3DomList( "exclusions" ); list.addChild( exclude ); Xpp3Dom newDependency = new Xpp3Dom( "dependency" ); newDependency.addChild( list ); // add exclusion to top-most dependency Xpp3DomHelper.mergeXpp3Dom( dependencies.getChild( 0 ), newDependency ); } /** * {@inheritDoc} */ public boolean removeExclusion( String groupId, String artifactId ) { boolean updated = false; String exclusionPath = "dependencies/dependency/exclusions/exclusion"; String xpath1 = exclusionPath + "[groupId='" + groupId + "' and artifactId='" + artifactId + "']"; updated = findChildren( xpath1, true ) || updated; String xpath2 = "dependencyManagement/" + xpath1; updated = findChildren( xpath2, true ) || updated; return updated; } /** * {@inheritDoc} */ public Properties getProperties() { Properties properties = new Properties(); Xpp3Dom map = m_pom.getChild( "properties" ); if( null != map ) { Xpp3Dom[] entries = map.getChildren(); for( int i = 0; i < entries.length; i++ ) { properties.setProperty( entries[i].getName(), entries[i].getValue() ); } } return properties; } /** * {@inheritDoc} */ public void setProperty( String key, String value ) { Xpp3Dom map = m_pom.getChild( "properties" ); if( null == map ) { map = new Xpp3Dom( "properties" ); m_pom.addChild( map ); } Xpp3Dom entry = new Xpp3Dom( key ); entry.setValue( value ); map.addChild( entry ); } /** * {@inheritDoc} */ public boolean updatePluginVersion( String groupId, String artifactId, String newVersion ) { boolean updated = false; String plugins = "plugins/plugin[groupId='" + groupId + "' and artifactId='" + artifactId + "']"; updated = updateVersion( "build/" + plugins, newVersion ) || updated; updated = updateVersion( "build/pluginManagement/" + plugins, newVersion ) || updated; return updated; } /** * {@inheritDoc} */ public void mergeSection( Pom pom, String fromSection, String toSection, boolean append ) { if( !( pom instanceof XppPom ) ) { throw new IllegalArgumentException( "Unable to merge POM type " + pom.getClass() ); } mergeSection( ( (XppPom) pom ).m_pom, fromSection, toSection, append ); } /** * Merge a section of XML from another XML fragment * * @param from another XML fragment * @param fromSection path to XML section to merge from * @param toSection path to XML section to merge into * @param append when true, append instead of merging */ private void mergeSection( Xpp3Dom from, String fromSection, String toSection, boolean append ) { String[] fromPath = fromSection.split( "/" ); // find source section Xpp3Dom source = from; for( int i = 0; i < fromPath.length; i++ ) { source = source.getChild( fromPath[i] ); if( null == source ) { return; } } if( append ) { Xpp3DomList.makeIntoList( source ); } Xpp3Dom project = new Xpp3Dom( "project" ); // create skeleton template Xpp3Dom skeleton = project; if( toSection != null ) { String[] toPath = toSection.split( "/" ); for( int i = 0; i < toPath.length; i++ ) { Xpp3Dom temp = new Xpp3Dom( toPath[i] ); skeleton.addChild( temp ); skeleton = temp; } } // add source to template skeleton.addChild( source ); m_pom = Xpp3DomHelper.mergeXpp3Dom( m_pom, project ); } /** * {@inheritDoc} */ public void overlayDetails( Pom pom ) { if( !( pom instanceof XppPom ) ) { throw new IllegalArgumentException( "Unable to overlay POM type " + pom.getClass() ); } Xpp3Dom overlay = ( (XppPom) pom ).m_pom; Xpp3Dom project = new Xpp3Dom( "project" ); // record before we drop any elements List newModules = pom.getModuleNames(); // avoid corruption of key elements removeProtectedElements( overlay ); Xpp3Dom[] sections = m_pom.getChildren(); for( int i = 0; i < sections.length; i++ ) { // provide basic XML framework underneath the overlay if( null == overlay.getChild( sections[i].getName() ) ) { project.addChild( sections[i] ); } } Xpp3Dom originalPom = new Xpp3Dom( m_pom ); m_pom = Xpp3DomHelper.mergeXpp3Dom( project, overlay ); // we want to keep these plugins exactly as they were in the original Pax-Construct v2 POMs String plugins = "plugins/plugin[artifactId='maven-bundle-plugin' or artifactId='maven-pax-plugin']"; findChildren( "build/" + plugins, true ); mergeSection( originalPom, "build/plugins", "build", true ); findChildren( "build/pluginManagement/" + plugins, true ); mergeSection( originalPom, "build/pluginManagement/plugins", "build/pluginManagement", true ); // merge properties - customized values take precedence mergeSection( originalPom, "properties", null, false ); // new modules go below existing infrastructure entries for( Iterator i = newModules.iterator(); i.hasNext(); ) { String module = (String) i.next(); if( new File( getBasedir(), module ).exists() ) { addModule( module, true ); } } } /** * @param fragment existing XML fragment */ private static void removeProtectedElements( Xpp3Dom fragment ) { List protectedElements = Arrays.asList( new String[] { "modelVersion", "parent", "artifactId", "groupId", "version", "packaging", "modules" } ); Xpp3Dom[] elements = fragment.getChildren(); for( int i = elements.length - 1; i >= 0; i-- ) { // we don't want these protected elements customized if( protectedElements.contains( elements[i].getName() ) ) { fragment.removeChild( i ); } } } /** * {@inheritDoc} */ public void write() throws IOException { String encoding = StreamFactory.getXmlEncoding( m_file ); Writer writer = StreamFactory.newXmlWriter( m_file ); XmlSerializer serializer = RoundTripXml.createSerializer(); serializer.setOutput( writer ); serializer.startDocument( encoding, null ); m_pom.writeToSerializer( null, serializer ); serializer.endDocument(); IOUtil.close( writer ); } /** * Local utility class to help construct a "map" style XML fragment */ private static class Xpp3DomMap extends Xpp3Dom { /** * Create a new map fragment * * @param elementName name of the map element */ public Xpp3DomMap( String elementName ) { super( elementName ); } /** * Add a mapping to the map * * @param elementName element name * @param elementValue element value */ public void putValue( String elementName, String elementValue ) { putValue( this, elementName, elementValue ); } /** * @param map map fragment * @param name element name * @param value element value */ static void putValue( Xpp3Dom map, String name, String value ) { if( null != value ) { // only store non-null mapppings Xpp3Dom child = new Xpp3Dom( name ); child.setValue( value ); map.addChild( child ); } } } /** * Private utility class to help construct a "list" style XML fragment */ private static class Xpp3DomList extends Xpp3Dom { /** * Create a new list fragment * * @param elementName name of the list element */ public Xpp3DomList( String elementName ) { super( elementName ); makeIntoList( this ); } /** * Switch an existing XML fragment to use the "list" style * * @param fragment existing XML fragment */ public static void makeIntoList( Xpp3Dom fragment ) { // list fragments must append their children when merging with other XML fragments fragment.setAttribute( CHILDREN_COMBINATION_MODE_ATTRIBUTE, CHILDREN_COMBINATION_APPEND ); } /** * Support addition of XML nodes at specific positions * * @param parent parent node * @param index index at which the child is to be inserted * @param child child node */ public static void addChild( Xpp3Dom parent, int index, Xpp3Dom child ) { int count = parent.getChildCount(); // basic API adds to end parent.addChild( child ); for( int i = index; i < count; i++ ) { // shuffle round like a circular buffer Xpp3Dom temp = parent.getChild( index ); parent.removeChild( index ); parent.addChild( temp ); } } } /** * Local utility method to check for child elements based on a simple XPATH query * * @param xpath simple XPATH query * @param clear remove matching elements * @return true if any child elements matched, otherwise false */ private boolean findChildren( String xpath, boolean clear ) { XppPathQuery pathQuery = new XppPathQuery( xpath ); Xpp3Dom parent = pathQuery.queryParent( m_pom ); if( null == parent ) { return false; } int[] children = pathQuery.queryChildren( parent ); if( clear ) { // sort ascending order Arrays.sort( children ); // now remove in reverse in case array shrinks for( int i = children.length - 1; i >= 0; i-- ) { parent.removeChild( children[i] ); } } return children.length > 0; } /** * {@inheritDoc} */ public boolean equals( Object obj ) { if( obj instanceof XppPom ) { return getId().equals( ( (XppPom) obj ).getId() ); } return false; } /** * {@inheritDoc} */ public int hashCode() { return getId().hashCode(); } /** * {@inheritDoc} */ public String toString() { return getId(); } }