Context

Related JIRA Issues

Related Maven 2.0.x Feature Branch

Discussion

The various publications that talk about Maven Best practices including Maven: The Definitive Guide and Better Builds with Maven recommend using variables in the declaration of the artifactId, groupId, and/or version depending on the circumstances. Maven currently allows variables to be used in the artifactId, groupId and version of a project (as well as many other places). Maven does not allow variables to be specified in any of the elements within the parent element. When installing or deploying a project Maven will copy the pom.xml for the project into the local or remote repository. No changes are made to the file.

Problem

There are actually two distinct problems at play here.

  1. Maven currently does not allow variables in any of the elements within the parent section. MNG-624 in particular focuses on the version element. In a multiproject build it is common for all the subprojects to inherit from the same parent. Frequently the parent (or some other ancestor) identifies the specific versions of dependent artifacts. When a change occurs, such as updating a dependency version, the version of the parent must be changed. This requires that each subproject pom must also be changed to reflect the new parent version. This is tedious, time consuming and very annoying. In fact, this has become the most asked for feature in Maven.
  2. When maven installs or deploys an artifact the pom is copied from the source directory to the local or remote repository without modification. So a pom that looks like
    <project>
    ...
      <groupId>org.apache.maven</groupId>
      <artifactId>demo</groupId>
      <version>${myVersion}</version>
    </project>
    
    and where myVersion is set to 1.0.0 will be deployed to /org/apache/maven/demo/1.0.0/demo-1.0.0.pom and will still contain ${myVersion} as the "value" of the version element. This could lead to the odd case of a dependency being specified with 1.0.0 as the version and myVersion never being specified, or worse being set to a value other than 1.0.0.  If one were to assume that item 1 above was the be fixed then this issue must also be addressed. If a project is deployed with its parent version (or any of the other parent elements) set to variables then it is impossible to guarantee that the build will always be reproducable since the parent version variable would have to be set either in a settings file or from the command line, neither of which can be guaranteed from one build to the next. Worse, any project that has a dependency on such a project would also have to define the variable in a settings file or on the command line.

Proposed Solution

Overview

The solution consists of two parts

  1. Allow the version element in the parent section to either be omitted or have its value be a variable. If the version element is omitted or the value of the variable cannot be determined than the parent pom is located using the relativePath and its version is used. If the version is specied as a variable then that version is used if it is present. Generally, it is preferred to not define the version variable and just let the parent be determined by locating the pom on disk. However, some project owners may wish to allow their subprojects to be independently retrieved and then be built without requiring the whole project be present on disk. In that case the parent project version needs to be specified as a variable and only in this case should it be specified on the command line or in a settings file.
  2. Interpolate the project's artifactId, groupId, version and parent version (if a parent is specified), update the project's pom.xml only for those 4 fields and save the updated file in the project's target directory. The updated pom.xml will be used when the install or deploy goals are run and when a child artifact references the project as a parent.

The first part of this enhancement is to determine what the parent pom's version is without it being hardcoded. To accomplish this a new method, resolveParent, was added to DefaultMavenProjectBuilder. It locates the parent's version by:

  • If the parent has a version and the version doesn't contain a variable then the value is the version.
  • Trying to interpolate the version. If a variable is present and it is defined as a system property or as a setting property then that version will be used. In practice it is expected that this will rarely succeed.
  • Looking up the parent in a new project cache where the key is the path to the project file. As projects are processed in a multiproject build they are added to the cache. Siblings or children will then find them in the cache by using the project's path along with the parent's relativePath value to compute the cache key.
  • If the parent is not in the cache then perhaps the build is for a subproject of a multiproject build. First look at the target directory for the parent's relative path. If a pom.xml is there use it. If not, then look at the relative path directory.
  • If a valid version wasn't found by any of these techniques then throw an exception.

These steps require that if it is desired to build a single subproject and the parent pom contains a variable for it's version then the parent project must be processed by maven using any goal besides clean (thus causing the pom to be published to the parent's target directory) before the child project can be processed. This requirement would be recursive as the only way to process the parent project is if it's parent is also fully resolved. 

As a concrete example consider the following projects:

/project/pom.xml

<project>
  <groupId>org.apache.maven</groupId>
  <artifactId>parent</artifactId>
  <version>1.0</version>
</project>

/project/child/pom.xml

<project>
  <artifactId>child</artifactId>
  <parent>
    <groupId>org.apache.maven</groupId>
    <artifactId>parent</artifactId>
    <version>1.0</version>
  <parent>
</project>

/project/child/grandchild/pom.xml

<project>
  <artifactId>grandchild</artifactId>
  <version>1.0</version>
  <parent>
    <groupId>org.apache.maven</groupId>
    <artifactId>child</artifactId>
  <parent>
</project>


In this case an attempt to build the grandchild before building the child will fail since the child does not contain its own version. However, building the child without building the parent would succeed, if the parent directory contains the pom.xml since the parent pom identifies its own version. Once a maven goal is run on the child project its target directory will then contain a pom.xml that looks like:

<project>
  <groupId>org.apache.maven</groupId>
  <artifactId>child</artifactId>
  <version>1.0</version>
  <parent>
    <groupId>org.apache.maven</groupId>
    <artifactId>parent</artifactId>
    <version>1.0</version>
  <parent>
</project>

Since this pom contains its own version the grandchild can now successfully determine the version of its parent and will be able to proceed.

The second aspect of this enhancement concerns modifying the pom so that the required elements are resolved before they are deployed to a repository. A number of challenges had to be overcome to make this work in a reasonable manner. First, it was desired to confine the scope of the change project resolution to the smallest amount of code possible. The proposed fix confines the change to within DefaultMavenProjectBuilder and MavenProject. MavenProject now keeps track of the original pom location as well as the updated pom if an update was performed. The artifact handling did not require any modification since the project's file attribute will now refer to the updated pom.

A second challenge was with writing the new project. Most code within Maven is using the MavenXPP3Writer. Unfortunately, using this creates a pom.xml that bears very little resemblence to the original pom file. Instead, a new class, DefaultModelTransformer, was created. The original pom.xml is parsed into a DOM tree, the tree is then updated as required and then the DOM tree is written as a new pom.xml in the target directory. Thus the updated file is extremely similar to the original file except for the updates.

Another challenge with writing the new pom.xml was when it could be created. Most of DefaultMavenProjectBuilder executes before any goals. In the first attempts at this fix the new pom.xml was created near the end of the buildInternal method. This worked fine - until a "mvn clean install" was tried. The new pom.xml file was created then clean removed it and the install failed because it couldn't locate the file being installed.  As a solution to this Maven event handling was enhanced somewhat. Currently, event handling is only used to print out the goals being executed. With this change the event handler now also is passed the event "source" - which is usually (but not always) the MavenProject. A new EventMonitor was created that calls methods in DefaultMavenProjectBuilder that allows it to keep track of whether the project has been "prepared" (i.e. the model was interpolated and the updated pom written to the target directory). The project is marked as not prepared when clean is run. When any goal other than clean is called the project is prepared.

The next challenge was determining what fields in the Model needed to be updated, how to update them efficiently, how to update the fields in the pom.xml and how to add them to the file if they did not exist.  To handle this the ModelItem class was created. The ModelItem constructor takes 4 parameters, the XPath expression to the location in the pom.xml, the expression to access the attribute in the Model or objects associated with the Model, the XPath expression that identifies the item to be used as a base for inserting a new item, and an Enum identifying whether the new item should be added before or after the item located via the previous parameter.

DefaultModelTransformer identifies the 4 items that are used in the proposed fix. They are declared as

items = new ModelItem[] {
            new ModelItem("/project/parent/version", "parent.version", "/project/parent/artifactId", ModelItem.Location.AFTER),
            new ModelItem("/project/groupId", "groupId", "/project/artifactId", ModelItem.Location.BEFORE),
            new ModelItem("/project/artifactId", "artifactId"),
            new ModelItem("/project/version", "version", "project/artifactId", ModelItem.Location.AFTER)
          };

The performance of setting and getting the Model items was optimized by creating an Array of Methods in the ModelItem constructor. So when a value is retrieved or stored the Methods in the array are called until they return null (if a getter) or the end of the list is reached.

Implementation Details

Source File

Status

Description

pom.xml

Modified

The java source and target versions were changed to 1.5 since this enhancement leverages and requires Java 5

MavenProject

Modified

Added originalFile and prepared attributes. OriginalFile will contain the File for the original pom. prepared is a boolean attribute that identifies whether the pom has been interpolated to resolve its "ModelItems" (see ModelItem and ModelTransformer below).

MavenEvent

New

Extends java.util.EventObject. Now used in event processing to allow the event "source" to be passed to the EventMonitor.

EventDispatcher

Modified

Passes a MavenEvent instead of a String to the event methods.

DefaultEventDispatcher

Modified

Passes a MavenEvent instead of a String to the event methods.

EventMonitor

Modified

Passes a MavenEvent instead of a String to the event methods.

AbstractSelectiveEventMonitor

Modified

Accepts a MavenEvent instead of a String in its event methods.

DefaultEventMonitor

Modified

Accepts a MavenEvent instead of a String in its event methods.

ProjectEventMonitor

New

Calls DefaultMaventProjectBuilder to prepare if the goal being started is not clean and to "unprepare" when the just completed goal was clean.

ModelItem

New

Identifies an attribute in the Model or in an Object associated with the Model that should be interpolated and transformed.

ModelTransformer

New

Interface that defines two methods; transformModel which updates the pom.xml and writes it to the target directory and resolveModel that interpolates the ModelItems in the Model.

DefaultModelTransformer

New

ModelTransformer implementation.

MavenProjectBuilder

Modified

Added prepareProject and cleanProject methods to the interface.

DefaultMavenProjectBuilder

Modified

  • Added fileProjectCache to cache projects using the path to the pom.xml as the key.
  • Added ModelTransformer attribute.
  • Added prepareProject and cleanProject methods.
  • Added resolveParent to resolve the Parent's version.
  • Moved code to retrieve parent project via the relative path into method ModelWithFile. This is called from both assembleLineage and resolveParent.

DefaultLifecycleExecutor

Modified

Pass a MavenEvent instead of a String

DefaultMaven

Modified

Pass a MavenEvent instead of a String

PluginParameterExpressionEvaluator

Modified

Call project.getBasedir() instead of projectFile.getParentFile()

ProjectSorter

Modified

Call project.getBasedir() instead of projectFile.getParentFile()

Notes

  1. This implementation limits the changes to the deployed pom to the project's artifactId, groupId, version and parent version elements. There are certainly other variables that could or perhaps should be replaced before the pom is installed or deployed. For example, it could be argued that dependency versions should be replaced.  However, there are cases where this may be considered undesirable. Since the overall goal of this fix was to allow the parent version to be either omitted or specified as a variable, it is more appropriate to consider those cases under a seperate enhancement request.
  2. The artifactId and groupId of the parent element still cannot be variables with this change. While that you easily be done no use cases identifying why it is necessary or even desirable were provided in the associated Jira issues.
  3. The only incompatibility this change introduces is with respect to how plugins determine the base directory. Even though MavenProject has contained a getBasedir() method for at least several years some plugins are still calling getFile().getParent() with the expection that the result will be the base directory. With this change that will no longer be the case. Any code doing this must be modified to call getBasedir() instead.
  • No labels