package org.apache.maven;
/* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.commons.jelly.JellyContext;
import org.apache.commons.jelly.expression.CompositeExpression;
import org.apache.commons.jelly.expression.Expression;
import org.apache.commons.jelly.expression.jexl.JexlExpressionFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.maven.jelly.JellyUtils;
import org.apache.maven.jelly.MavenJellyContext;
import org.apache.maven.project.Project;
import org.apache.tools.ant.DirectoryScanner;
import org.codehaus.plexus.util.CollectionUtils;
import org.codehaus.plexus.util.StringUtils;
import org.xml.sax.SAXException;
import com.werken.forehead.ForeheadClassLoader;
import java.beans.IntrospectionException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.StringTokenizer;
import javax.xml.parsers.ParserConfigurationException;
/**
* Utilities for reading maven project descriptors, profile descriptors and workspace descriptors.
*
* @author <a href="mailto:jason@zenplex.com">Jason van Zyl</a>
* @author <a href="mailto:brett@apache.org">Brett Porter</a>
*
* @todo Remove all the context creation code and make a ContextBuilder class.
* @todo [RC2] split getProject (project.properties + defaults) != getPluginProject (plugin.properties only)
*/
public class MavenUtils
{
/** Log. */
private static final Log LOGGER = LogFactory.getLog( MavenUtils.class );
/** Map of loaded POMs. */
private static HashMap parentPoms = new HashMap();
/** Maven unknown error message. */
public static final String MAVEN_UNKNOWN_ERROR = "Unknown error reading project";
/**
* Create a Project object given a file descriptor.
*
* @param projectDescriptor
* a maven project.xml
* @return the Maven project object for the given project descriptor
* @throws MavenException
* when any errors occur
*/
public static Project getProject( File projectDescriptor ) throws MavenException
{
return getProject( projectDescriptor, null );
}
/**
* Create a Project object given a file descriptor, and a parent context
*
* @param projectDescriptor
* The file to create the project from
* @param parentContext
* the parent Maven Jelly Context
* @return a new Project
* @throws MavenException
* when any error happens.
*/
public static Project getProject( File projectDescriptor, MavenJellyContext parentContext ) throws MavenException
{
return getProject( projectDescriptor, parentContext, true );
}
/**
* Create a Project object given a file descriptor and optionally a parent Jelly context.
*
* @param projectDescriptor
* a maven project.xml {@link File}
* @param parentContext
* the parent context for the new project
* @param useParentPom
* whether a parent project should be respected
* @return the MavenSession project object for the given project descriptor
* @throws MavenException
* when any errors occur
*/
public static Project getProject( File projectDescriptor, MavenJellyContext parentContext, boolean useParentPom )
throws MavenException
{
Project project = null;
try
{
project = getNonJellyProject( projectDescriptor, parentContext, useParentPom );
project = getJellyProject( project );
project.setFile( projectDescriptor );
// Fully initialize the project.
project.initialize();
}
catch ( IntrospectionException e )
{
throw new MavenException( "Error creating a string from the project", e );
}
catch ( IOException e )
{
throw new MavenException( "Error reading XML or initializing", e );
}
catch ( ParserConfigurationException e )
{
throw new MavenException( "Error creating a JAXP Parser", e );
}
catch ( SAXException e )
{
throw new MavenException( "Error parsing XML", e );
}
catch ( MavenException e )
{
throw e;
}
catch ( Exception e )
{
// FIXME
throw new MavenException( MAVEN_UNKNOWN_ERROR, e );
}
return project;
}
/**
* Get a project, but not a Jelly-ised project. ie Don't evaluate the variables. We are doing several things when
* creating a POM object, the phases are outlined here:
*
* 1) The project.xml file is read in using betwixt which creates for us a Project object that, at this point, has
* not been run through Jelly i.e. no interpolation has occured yet.
*
* 2) The context for the project is created and set. So each project manages its own context. See the
* createContext() method for the details context creation process.
*
* 3) We check to see if the <extend> tag is being employed. If so, the parent project.xml file is read in. At
* this point we have a child and parent POM and the values are merged where the child's values override those of
* the parent.
*
* @param projectDescriptor
* the project file
* @param parentContext
* the parent context for the new project
* @param useParentPom
* whether a parent project should be respected
* @return the project
* @throws MavenException
* when there are errors reading the descriptor
* @throws IOException
* when resolving file names and paths
*/
public static Project getNonJellyProject( File projectDescriptor, MavenJellyContext parentContext,
boolean useParentPom ) throws MavenException, IOException
{
// 1)
Project project = null;
try
{
project = new Project( projectDescriptor.toURL() );
}
catch ( Exception e )
{
throw new MavenException( "Error parsing project.xml '" + projectDescriptor.getAbsolutePath() + "'", e );
}
// 2)
MavenJellyContext context =
MavenUtils.createContextNoDefaults( projectDescriptor.getParentFile(), parentContext );
// 3)
String pomToExtend = project.getExtend();
if ( ( pomToExtend != null ) && useParentPom )
{
// We must look in the <extend/> element for expressions that may be present as
//
// <extend>../project.xml</extend>
Expression e = JellyUtils.decomposeExpression( pomToExtend, context );
pomToExtend = e.evaluateAsString( context );
pomToExtend = MavenUtils.makeAbsolutePath( projectDescriptor.getParentFile(), pomToExtend );
project.setExtend( pomToExtend );
File parentPom = new File( pomToExtend );
parentPom = parentPom.getCanonicalFile();
if ( !parentPom.exists() )
{
throw new FileNotFoundException( "Parent POM not found: " + parentPom );
}
String parentPomPath = parentPom.getPath();
if ( parentPomPath.equals( projectDescriptor.getCanonicalPath() ) )
{
throw new MavenException( "Parent POM is equal to the current POM" );
}
Project parent = (Project) parentPoms.get( parentPomPath );
if ( parent == null )
{
parent = getNonJellyProject( parentPom, parentContext, true );
parent.setFile( parentPom );
parentPoms.put( parentPom.getCanonicalPath(), parent );
context.setParent( parent.getContext() );
}
// Map in the parent context which already has the properties loaded
integrateMapInContext( parent.getContext().getVariables(), context );
project.mergeParent( parent );
}
project.resolveIds();
applyDefaults( context );
// Set the created context, and put the project itself in the context. This
// is how we get the ${pom} reference in the project.xml file to work.
project.setContext( context );
context.setProject( project );
return project;
}
/**
* This is currently used for the reactor but may be generally useful.
*
* @param directory
* the directory to scan for maven projects
* @param includes
* the pattern that matches a project that you want included
* @param excludes
* the pattern that matches a project that you don't want included
* @return a {link List} of {@link Project}s
* @throws MavenException
* when anything goes wrong.
*/
public static List getProjects( File directory, String includes, String excludes ) throws MavenException
{
return getProjects( directory, includes, excludes, null );
}
/**
* This is currently used for the reactor but may be generally useful.
*
* @param directory
* the directory to scan for maven projects
* @param includes
* Patterns to include.
* @param excludes
* Patterns to exclude.
* @param context
* the parent context
* @return a {link List} of {@link Project}s
* @throws MavenException
* when anything goes wrong.
*/
public static List getProjects( File directory, String includes, String excludes, MavenJellyContext context )
throws MavenException
{
String[] files = getFiles( directory, includes, excludes );
List projects = new ArrayList();
for ( int i = 0; i < files.length; i++ )
{
Project p = getProject( new File( files[i] ), context );
projects.add( p );
}
return projects;
}
/**
* Take the POM and interpolate the value of the project's context to create a new version of the POM with expanded
* context values.
*
* @param project
* the maven POM
* @return Jelly interpolated project.
* @throws Exception
* when there are errors reading FIXME
*/
public static Project getJellyProject( Project project ) throws Exception
{
// Keep a copy of the original context
MavenJellyContext originalContext = project.getContext();
// We don't want any taglib references in the context or Jelly
// gets confused. All we want are the variables for interpolation. We
// can change this but I think we would like to avoid general Jelly
// idiom in the POM anyway.
JellyContext context = new JellyContext();
JellyUtils.populateVariables( context, originalContext );
// We don't want the context or the parent being written out into the XML which
// is the interpolated POM.
project.setContext( null );
Project parent = project.getParent();
project.setParent( null );
// Interpolate
project = getInterpolatedPOM( project, context );
// Restore parent and context
project.setParent( parent );
project.setContext( originalContext );
project.getContext().setProject( project );
return project;
}
/**
* Get the POM with all variables resolved.
*
* @param project
* the project to resolve
* @param context
* the context to retrieve variables from
* @return a project with no unresolved elements.
* @throws Exception
* if there is an error parsing the project FIXME
*/
private static Project getInterpolatedPOM( Project project, JellyContext context ) throws Exception
{
String projectString = project.getProjectAsString();
Expression e = JellyUtils.decomposeExpression( projectString, context );
String newProjectString = e.evaluateAsString( context );
// We can use a Reader and not an URL/path here to read
// the POM because this is a memory model without XML entities.
project = new Project( new StringReader( newProjectString ) );
return project;
}
/**
* Get a set of files from a specifed directory with a set of includes.
*
* @param directory
* Directory to scan.
* @param includes
* Comma separated list of includes.
* @return files
*/
public static String[] getFiles( File directory, String includes )
{
return getFiles( directory, includes, null );
}
/**
* Get a set of files from a specifed directory with a set of includes.
*
* @param directory
* Directory to scan.
* @param includes
* Comma separated list of includes.
* @param excludes
* Comma separated list of excludes.
* @return files
*/
public static String[] getFiles( File directory, String includes, String excludes )
{
String[] includePatterns = null;
if ( includes != null )
{
includePatterns = StringUtils.split( includes, "," );
}
String[] excludePatterns = null;
if ( excludes != null )
{
excludePatterns = StringUtils.split( excludes, "," );
}
DirectoryScanner directoryScanner = new DirectoryScanner();
directoryScanner.setBasedir( directory );
directoryScanner.setIncludes( includePatterns );
directoryScanner.setExcludes( excludePatterns );
directoryScanner.scan();
String[] files = directoryScanner.getIncludedFiles();
for ( int i = 0; i < files.length; i++ )
{
files[i] = new File( directory, files[i] ).getAbsolutePath();
}
return files;
}
/**
* Take a dominant and recessive Map and merge the key:value pairs where the recessive Map may add key:value pairs
* to the dominant Map but may not override any existing key:value pairs.
*
* If we have two Maps, a dominant and recessive, and their respective keys are as follows:
*
* dominantMapKeys = { a, b, c, d, e, f } recessiveMapKeys = { a, b, c, x, y, z }
*
* Then the result should be the following:
*
* resultantKeys = { a, b, c, d, e, f, x, y, z }
*
* @param dominantMap
* Dominant Map.
* @param recessiveMap
* Recessive Map.
* @return The result map with combined dominant and recessive values.
*/
public static Map mergeMaps( Map dominantMap, Map recessiveMap )
{
Map result = new HashMap();
if ( ( dominantMap == null ) && ( recessiveMap == null ) )
{
return null;
}
if ( ( dominantMap != null ) && ( recessiveMap == null ) )
{
return dominantMap;
}
if ( dominantMap == null )
{
return recessiveMap;
}
// Grab the keys from the dominant and recessive maps.
Set dominantMapKeys = dominantMap.keySet();
Set recessiveMapKeys = recessiveMap.keySet();
// Create the set of keys that will be contributed by the
// recessive Map by subtracting the intersection of keys
// from the recessive Map's keys.
Collection contributingRecessiveKeys =
CollectionUtils.subtract( recessiveMapKeys,
CollectionUtils.intersection( dominantMapKeys, recessiveMapKeys ) );
result.putAll( dominantMap );
// Now take the keys we just found and extract the values from
// the recessiveMap and put the key:value pairs into the dominantMap.
for ( Iterator i = contributingRecessiveKeys.iterator(); i.hasNext(); )
{
Object key = i.next();
result.put( key, recessiveMap.get( key ) );
}
return result;
}
/**
* Take a series of <code>Map</code>s and merge them where the ordering of the array from 0..n is the dominant
* order.
*
* @param maps
* An array of Maps to merge.
* @return Map The result Map produced after the merging process.
*/
public static Map mergeMaps( Map[] maps )
{
Map result;
if ( maps.length == 0 )
{
result = null;
}
else if ( maps.length == 1 )
{
result = maps[0];
}
else
{
result = mergeMaps( maps[0], maps[1] );
for ( int i = 2; i < maps.length; i++ )
{
result = mergeMaps( result, maps[i] );
}
}
return result;
}
/**
* Load the build.properties file for a project.
*
* @param directory
* the directory of the project
* @return the properties
*/
private static Properties loadProjectBuildProperties( File directory )
{
// project build properties
File projectBuildPropertiesFile = new File( directory, "build.properties" );
LOGGER.debug( "Using projectBuildPropertiesFile: " + projectBuildPropertiesFile.getAbsolutePath() );
return loadProperties( projectBuildPropertiesFile );
}
/**
* Load the project.properties file for a project.
*
* @param directory
* the directory of the project
* @return the properties
*/
private static Properties loadProjectProperties( File directory )
{
// project properties
File projectPropertiesFile = new File( directory, "project.properties" );
LOGGER.debug( "Using projectPropertiesFile: " + projectPropertiesFile.getAbsolutePath() );
return loadProperties( projectPropertiesFile );
}
/**
* Create a jelly context given a descriptor directory.
*
* @param descriptorDirectory
* The directory from which to pull the standard maven properties files from.
* @return The generated maven based on the contents of the standard maven properties files.
*/
public static MavenJellyContext createContext( File descriptorDirectory )
{
return createContext( descriptorDirectory, null );
}
/**
* Create a jelly context given a descriptor directory and parent jelly context.
*
* @param descriptorDirectory
* The directory from which to pull the standard maven properties files from.
* @param parentContext
* The parent jelly context.
* @todo should premerge driver, etc if they are being kept
* @return The generated maven based on the contents of the standard maven properties files.
*/
public static MavenJellyContext createContext( File descriptorDirectory, MavenJellyContext parentContext )
{
MavenJellyContext context = createContextNoDefaults( descriptorDirectory, parentContext );
applyDefaults( context );
return context;
}
/**
* Create a jelly context given a descriptor directory and parent jelly context, but don't apply any defaults.
*
* @param descriptorDirectory
* The directory from which to pull the standard maven properties files from.
* @param parentContext
* The parent jelly context.
* @todo should premerge driver, etc if they are being kept
* @return The generated maven based on the contents of the standard maven properties files.
*/
private static MavenJellyContext createContextNoDefaults( File descriptorDirectory, MavenJellyContext parentContext )
{
// System properties
Properties systemProperties = System.getProperties();
// User build properties
File userBuildPropertiesFile = new File( System.getProperty( "user.home" ), "build.properties" );
LOGGER.debug( "Using userBuildPropertiesFile: " + userBuildPropertiesFile.getAbsolutePath() );
Properties userBuildProperties = loadProperties( userBuildPropertiesFile );
Properties projectProperties = loadProjectProperties( descriptorDirectory );
Properties projectBuildProperties = loadProjectBuildProperties( descriptorDirectory );
Properties driverProperties =
loadProperties( MavenUtils.class.getResourceAsStream( MavenConstants.DRIVER_PROPERTIES ) );
Map result =
MavenUtils.mergeMaps( new Map[] { systemProperties, userBuildProperties, projectBuildProperties,
projectProperties, driverProperties } );
MavenJellyContext context;
if ( parentContext != null )
{
context = new MavenJellyContext( parentContext );
}
else
{
context = new MavenJellyContext();
}
// Turn off inheritence so parent values are overriden
context.setInherit( false );
// integrate everything else...
MavenUtils.integrateMapInContext( result, context );
// Turn inheritance back on to make the parent's values visible.
context.setInherit( true );
// Set the basedir value in the context.
context.setVariable( "basedir", descriptorDirectory.getAbsolutePath() );
return context;
}
/**
* Apply default settings.
*
* @param context
* Jelly context to apply the defaults.
*/
private static void applyDefaults( MavenJellyContext context )
{
Properties defaultProperties =
loadProperties( MavenUtils.class.getResourceAsStream( MavenConstants.DEFAULTS_PROPERTIES ) );
// integrate defaults...
MavenUtils.integrateMapInContext( defaultProperties, context );
// deliberately use the original base directory for these variables
context.resolveRelativePaths( new File( System.getProperty( "user.dir" ) ) );
}
/**
* Integrate a Map of key:value pairs into a <code>MavenJellyContext</code>. The values in the Map may be
* <code>CompositeExpression</code>s that need to be evaluated before being placed into the context.
*
* @param map
* Map to integrate into the provided jelly context.
* @param context
* Jelly context to integrate the map into.
*/
public static void integrateMapInContext( Map map, MavenJellyContext context )
{
if ( map == null )
{
return;
}
JexlExpressionFactory factory = new JexlExpressionFactory();
for ( Iterator i = map.keySet().iterator(); i.hasNext(); )
{
String key = (String) i.next();
Object value;
// Parent contexts are already handled, so only concern ourselves with whether it exists in the current
// context
if ( context.getVariables().get( key ) == null )
{
value = map.get( key );
if ( value instanceof String )
{
try
{
String literalValue = (String) value;
Expression expr = CompositeExpression.parse( literalValue, factory );
if ( expr != null )
{
value = expr;
}
else
{
value = literalValue;
}
}
catch ( Exception e )
{
// do nothing.
LOGGER.debug( "Unexpected error evaluating expression", e );
}
}
context.setVariable( key, value );
}
}
}
/**
* Load properties from a <code>File</code>.
*
* @param file
* Propertie file to load.
* @return The loaded Properties.
*/
private static Properties loadProperties( File file )
{
FileInputStream fis = null;
try
{
if ( file.exists() )
{
fis = new FileInputStream( file );
return loadProperties( fis );
}
}
catch ( Exception e )
{
// ignore
LOGGER.debug( "Unexpected error loading properties", e );
}
finally
{
if ( fis != null )
{
try
{
fis.close();
}
catch ( IOException e )
{
LOGGER.debug( "WARNING: Cannot close stream!", e );
}
fis = null;
}
}
return null;
}
/**
* Load properties from an <code>InputStream</code>.
*
* @param is
* InputStream from which load properties.
* @return The loaded Properties.
*/
private static Properties loadProperties( InputStream is )
{
try
{
Properties properties = new Properties();
properties.load( is );
for ( Iterator i = properties.keySet().iterator(); i.hasNext(); )
{
String property = (String) i.next();
properties.setProperty( property, properties.getProperty( property ).trim() );
}
return properties;
}
catch ( IOException e )
{
// ignore
LOGGER.debug( "Unexpected exception loading properties", e );
}
finally
{
try
{
if ( is != null )
{
is.close();
}
}
catch ( IOException e )
{
// ignore
LOGGER.debug( "Unexpected exception loading properties", e );
}
}
return null;
}
/** Resource bundle with user messages. */
private static ResourceBundle messages;
/**
* Load MavenSession user messages from a resource bundle given the user's locale.
*
* @todo Move locale tools into their own class.
*/
private static void loadMessages()
{
try
{
// Look for the message bundle corresponding to the user's locale.
messages = ResourceBundle.getBundle( "org/apache/maven/messages/messages" );
}
catch ( MissingResourceException e )
{
// If we can't find the appropriate message bundle for the locale then
// we will fall back to English.
messages = ResourceBundle.getBundle( "org/apache/maven/messages/messages", Locale.ENGLISH );
}
}
/**
* Retrieve a user message.
*
* @param messageId
* Id of message type to use.
* @return Message for the user's locale.
*/
public static String getMessage( String messageId )
{
return getMessage( messageId, null );
}
/**
* Retrieve a user message.
*
* @param messageId
* Id of message type to use.
* @param variable
* Value to substitute for ${1} in the given message.
* @return Message for the user's locale.
*/
public static String getMessage( String messageId, Object variable )
{
if ( messages == null )
{
try
{
loadMessages();
}
catch ( MissingResourceException mre )
{
LOGGER.error( mre );
return messageId + ( variable == null ? "" : " " + variable );
}
}
if ( variable == null )
{
return messages.getString( messageId );
}
else
{
return StringUtils.replace( messages.getString( messageId ), "${1}", variable.toString() );
}
}
/**
* Resolve directory against a base directory if it is not already absolute.
*
* @param basedir
* the base directory for relative paths
* @param dir
* the directory to resolve
* @throws IOException
* if canonical path fails
* @return the canonical path of the directory if not absolute
*/
public static String makeAbsolutePath( File basedir, String dir ) throws IOException
{
File f = new File( dir );
if ( ( f.isAbsolute() ) )
{
return f.getCanonicalPath();
}
else
{
return new File( basedir, dir ).getCanonicalPath();
}
}
/**
* Convert an absolute path to a relative path if it is under a given base directory.
*
* @param basedir
* the base directory for relative paths
* @param path
* the directory to resolve
* @return the relative path
* @throws IOException
* if canonical path fails
*/
public static String makeRelativePath( File basedir, String path ) throws IOException
{
String canonicalBasedir = basedir.getCanonicalPath();
File pathFile = new File( path );
if ( !pathFile.isAbsolute() )
{
LOGGER.warn( "WARNING: path is not an absolute pathname! Returning path." );
return path;
}
String canonicalPath = pathFile.getCanonicalPath();
if ( canonicalPath.equals( canonicalBasedir ) )
{
return ".";
}
if ( canonicalPath.startsWith( canonicalBasedir ) )
{
if ( canonicalPath.charAt( canonicalBasedir.length() ) == File.separatorChar )
{
canonicalPath = canonicalPath.substring( canonicalBasedir.length() + 1 );
}
else
{
canonicalPath = canonicalPath.substring( canonicalBasedir.length() );
}
}
else
{
LOGGER.warn( "WARNING: path does not start with basedir! Returning path." );
return path;
}
return canonicalPath;
}
/**
* Get a list of goals from a CSV list.
*
* @param goalCsv
* the goals
* @return the list of goal names
*/
public static List getGoalListFromCsv( String goalCsv )
{
StringTokenizer tok = new StringTokenizer( goalCsv, "," );
List goals = new ArrayList();
while ( tok.hasMoreTokens() )
{
goals.add( tok.nextToken().trim() );
}
return goals;
}
/**
* Debugging function.
*
* @param classLoader
* the class loader
*/
public static void displayClassLoaderContents( ForeheadClassLoader classLoader )
{
LOGGER.info( "ClassLoader name: " + classLoader.getName() );
URL[] urls = classLoader.getURLs();
for ( int i = 0; i < urls.length; i++ )
{
LOGGER.info( "urls[" + i + "] = " + urls[i] );
}
ClassLoader parent = classLoader.getParent();
if ( ( parent != null ) && ( parent instanceof ForeheadClassLoader ) )
{
LOGGER.info( "Displaying Parent classloader: " );
displayClassLoaderContents( (ForeheadClassLoader) classLoader.getParent() );
}
}
}