Friday, December 17, 2010

Maven 3 & profiles.xml

There are some pretty cool things in Maven 3 (although mixins and global dependency exclusions have been tabled until 3.1). Matt Raible talks about some of them here. Significant points include dramatically increased performance (50% to 400% faster), unspecified plugin versions will pull the latest version and not snapshot versions (though it's best to be explicit about plugin versions), also Sonatype has developed Maven Shell and polyglot Maven to work with Maven 3.

There are, however, some compatibility concerns when moving from Maven 2.x. And not all plugins work yet. (Most notably they're refactoring the Site Plugin so it's not completely working yet). But the Maven team intends the new release to be usable as a drop-in replacement for Maven 2 (though this won't be the case for Maven 3.1).

Most changes for compatibility seemed pretty trivial. The biggest thing I see preventing Maven 3 from being a drop-in replacement for Maven 2 was their decision to remove support for profiles.xml. This was documented in MNG-4060, though not much discussed there. A few people have complained about this already here and it was discussed a little here. I haven't seen much justification for this, other than it's supposed to be difficult to test. Though I'm not sure why it'd be much harder to test than the use of profiles in the pom.xml. But the Maven team seems for whatever reason fairly committed to this idea, despite one of their committers (Milos Kleint) disagreeing with their position.

People use profiles for a variety of things. Where I work, they are commonly used for environment settings. For example, which version of a particular web service or database to use in prod, qa, etc. But also for which version of that service or database to use for a particular developer's sandbox (another common difference between developers is their log level). These typically use Maven filtering in conjunction with external profiles to accomplish this. These profile properties are also kept in our SCM so if something used by all developer's sandbox, it is easily changeable and transparent to all developers on the project. It is currently impossible to run Maven 3 in this way.

We are not entirely without options when it comes to addressing this, but none of the solutions in my opinion is as nice as the stable, built-in profiles.xml feature. Recently, I've asked if there is some other mechanism I should be using to accomplish this. To date, I've not seen anyone fully explore this issue that I think is important for Maven 3 going forward. So that's what I did here. If you'd like any of the sample projects I created for this exploration, just drop me a line. Maybe I'll put them up on GitHub or something at some point. So without further ado, here are the options I've found and their pros & cons.

1. Fork Maven and put the feature back in
Maybe it's my cynical nature, but this was actually the first thing that came to my mind. But I don't know that very many people would feel comfortable running a patched version of Maven. Plus, I'd want to keep forking them to keep getting all the other goodies they add, which makes a lot of work for me. Then I thought of submitting a patch to Maven for this. But when I found out the decision was pretty deliberate and not simply a lack of resources, I backed off that idea.
Pros Cons
It's the way Maven should be A lot of work
Who's brave enough to use it?

2. Stick with Maven 2
Hey, there's nothing wrong with being old fashioned. There's strong logic to the "If it ain't broke, don't fix it" argument. Some people are even still happily on the 2.0.x branch rather than the 2.2.x branch.
Pros Cons
The least work of any other solution No Maven 3 goodies

3. Place all profiles properties in the POM
POMs in Maven 3 still use a <modelVersion>4.0.0</modelVersion>. There for you could put everything inside it like
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.foo</groupId>
  <artifactId>pomProfileTest</artifactId>
  <version>1.0</version>

  <properties>
    <!-- these are defaults, they can be overridden with a settings.xml -->
    <javaVersion>1.5</javaVersion>
    <junitVersion>4.8.2</junitVersion>
    <sourceEncoding>UTF-8</sourceEncoding>
    <resourceEncoding>UTF-8</resourceEncoding>
    <profile>dev</profile>
    <prop1>null</prop1>
    <prop2>null</prop2>
  </properties>

  <profiles>
    <profile>
      <id>developer1</id>
      <properties>
        <prop1>prop1Value</prop1>
        <prop2>prop2Value</prop2>
      </properties>
    </profile>
    <profile>
      <id>developer2</id>
      <properties>
        <prop1>anotherProp1Value</prop1>
        <prop2>anotherProp2Value</prop2>
      </properties>
    </profile>
  </profiles>
<!-- ... -->
</project>
This could then be invoked in the standard way:
mvn -P developer1 <goal>
Pros Cons
Simple to implement: scripts can continue to invoke Maven in the current way Clutters POM, particularly troublesome since the POM is also deployed.
Also works with Maven 2
Variations
1. Multiple pom.xml files could be checked into the project to cut down on the clutter inside the main pom.xml.

4. Place all an environment's (or developer's) settings in a single settings.xml
This seems to be Maven's official answer on the subject. For things that are common across many projects, this might be a decent solution. But for the many project specific settings (e.g. a db.url property), you'd have to make sure they are named uniquely across all projects so as not to conflict with each other. This makes for a real maintenance problem.
As one commenter in the mailing list noted, some developers have a fear of changing their settings.xml (even if they should be comfortable with this) and would prefer to simply change a few properties. But this should make us pause to make sure there's nothing we're using profiles.xml for when we should actually be using settings.xml.
Pros Cons
Maven's official solution Uniquely named properties causes maintenance issues
Also works with Maven 2 Changes not normally visible to all developers

5. Use separate settings.xml files
You can specify a different user settings file with
mvn -s path/someSettingsFile.xml <goal>
and create a file like
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
                      http://maven.apache.org/xsd/settings-1.0.0.xsd">
  <profiles>
    <profile>
      <id>developer1</id>
      <properties>
        <!-- global properties -->
        <siteLocation>file://${user.dir}/site</siteLocation>

        <!-- project specific properties -->
        <prop1>prop1Value</prop1>
        <prop2>prop2Value</prop2>
      </properties>
      <!-- ... -->
    <profile>
  <profiles>
</settings>

Pros Cons
Little profile clutter in POM Massive clutter & overhead in profile settings file, as all your normal settings.xml things are now in each profile settings file
Checking all these settings files in allows developers to make sure they're all using and deploying to the same repos profile settings are machine-dependent since <localRepository/> is also in settings file.
Also works with Maven 2

6. Use the Properties Maven Plugin
The Properties Maven Plugin allows for properties files to be loaded (and saved) just as if you had used <properties/> in the pom.xml itself.
Pros Cons
Properties are cleanly externalized Plugin is considered an alpha version
Also works with Maven 2

Variations
1. Use profiles to select the properties file
This could be invoked in the standard way:
mvn -P developer1 <goal>
And your pom might look like
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.foo</groupId>
  <artifactId>propertiesProfileTest</artifactId>
  <version>1.0</version>

  <properties>
    <!-- these are defaults, they can be overridden with a settings.xml -->
    <javaVersion>1.5</javaVersion>
    <junitVersion>4.8.2</junitVersion>
    <sourceEncoding>UTF-8</sourceEncoding>
    <resourceEncoding>UTF-8</resourceEncoding>
    <profile>dev</profile>
    <prop1>null</prop1>
    <prop2>null</prop2>
  </properties>

  <profiles>
    <profile>
      <id>developer1</id>
      <properties>
        <profile>developer1.properties</profile>
      </properties>
    </profile>
    <profile>
      <id>developer2</id>
      <properties>
        <profile>developer2.properties</profile>
      </properties>
    </profile>
  </profiles>

  <build>
    <plugins>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>properties-maven-plugin</artifactId>
        <version>1.0-alpha-2</version>
        <executions>
          <execution>
            <phase>initialize</phase>
            <goals>
              <goal>read-project-properties</goal>
            </goals>
            <configuration>
              <files>
                <file>${project.basedir}/filters/${profile}</file>
              </files>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
  <!-- ... -->
</project>
Pros Cons
Standard way for selecting profile POM now is cluttered with mapping profiles to properties files

2. Use a variable to select the properties file
In my sample, the invocation would be
mvn -Dprofile=developer1 <goal>
And the POM would be like
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.foo</groupId>
  <artifactId>propertiesProfileTest</artifactId>
  <version>1.0</version>

  <properties>
    <!-- these are defaults, they can be overridden with a settings.xml -->
    <javaVersion>1.5</javaVersion>
    <junitVersion>4.8.2</junitVersion>
    <sourceEncoding>UTF-8</sourceEncoding>
    <resourceEncoding>UTF-8</resourceEncoding>
    <profile>dev</profile>
    <prop1>null</prop1>
    <prop2>null</prop2>
  </properties>

  <build>
    <plugins>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>properties-maven-plugin</artifactId>
        <version>1.0-alpha-2</version>
        <executions>
          <execution>
            <phase>initialize</phase>
            <goals>
              <goal>read-project-properties</goal>
            </goals>
            <configuration>
              <files>
                <file>${project.basedir}/filters/${profile}.properties</file>
              </files>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
  <!-- ... -->
</project>
Pros Cons
Little profile clutter in POM No standard way of selecting profile

Conclusion
I still really wish the Maven folks would change their mind on this. But until then (unless you're sticking with Maven 2 -- which might not be a bad idea at least for now because of plugin incompatibilities), the best option seems to me to use the Maven Properties Plugin, either with a property or with a profile. (Personally, I'm leaning to the profile option beacuse I think the fact it can be invoked in a standard way is worth the extra POM clutter). Though it is technically an alpha version, it seemed to work fine for me and I believe Maven's decision to remove support for profiles.xml will cause people to flock to this plugin, and therefore likely to only increase its stability. Of course, IDE support for this practice is another question.

Conclusion Part 2
I do feel obligated to mention that the practice of using Maven filtering for things like database URLs where I work is actually changing to use runtime properties instead. (Generally by building a property reader class using java.util.Properties to read different properties files based on the name passed with -Denv=<environmentName>). This has the advantage (besides working with Maven 3) of not requiring separate deployments just for environment differences or redeploys for changes to an environment property.
I still think removing profiles.xml support is bad since the intent was to keep backward compatibility, thus making pointing my M2_HOME to Maven 3 a bit painful when working on old and new projects. It also seems a bit strange that they removed support for profiles.xml when they but not for profiles. It was nice to be able to have those external to the POM.
However, for my (and probably the gentleman on Nabble also) use case, Maven filtering with profiles probably may not have been the right idea in the first place.