Skip to content

The Structure of centipede archetype

Paul Houle edited this page Dec 28, 2013 · 2 revisions

This page explains the structure of centipede archetype so that you can modify it if you need to or use the knowledge behind it to build other archetype plugins.

The role of filtering

The most complex thing going on in the archetype plugin is that filtering is happening at three different points in the process, which causes things to be shuffled into different directories at different times and also requiring careful attention payed to escaping.

Archetype build-time filtering

The build process for archetypes is typically pretty simple. A template for the project that will be created by the archetype is created in target/classes/archetype-resources and a configuration file that controls the archetype generation process goes to target/classes/META-INF/maven/archetype-metadata.xml.

For better and worse, we want the version number to be synchronized between centipede and centipede-archetype. In version 99.0 I hadn't paid attention to this, so the system incorrectly built a dependency on the 99.0-SNAPSHOT version of centipede into the pom.xml file. Although large systems become unmaintainable when version synchronization is forced between a large projects, the centipede system is small and the addition of a manual step in the release process will certainly lead to errors and suffering.

This filtering is enabled by the following maven snippets in the pom.xml for the archetype project:

  <build>
    <resources>
      <resource>
        <filtering>true</filtering>
        <directory>${basedir}/src/main/filtered</directory>
      </resource>
      <resource>
        <filtering>false</filtering>
        <directory>${basedir}/src/main/resources</directory>
      </resource>
    </resources>
    ...
    <plugins>
      ...
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-resources-plugin</artifactId>
        <version>2.6</version>
        <configuration>
          <escapeString>\</escapeString>
        </configuration>
      </plugin>
    </plugins>

Note that the pom.xml template for the generated project is put in the src\main\filtered directory. Normally we wouldn't need to explicitly declare the maven-resources-plugin, however, we need to do so in this case because we want to enable the backslash as an escape character. This lets us write ${version} in the pom.xml template to refer to the version of the archetype project and write \${version} to have {$version} written into the POM template which will later be interpolated with the version of the archetype project. Note that the maven-resources-plugin uses it's own system of string interpolation which looks a bit like Velocity but is nowhere near is full-featured.

All of the other files that go into the archetype JAR are in the src/main/resources directory and these are copied unfiltered into the project.

Filtering upon archetype generation

The bad news is that filtering is done differently upon archetype generation (what happens when you do mvn archetype:generate), but the good news is that this filtering is done with Velocity, which is really powerful.

The archetype generation process is controlled by this code from the target/classes/META-INF/maven/archetype-metadata.xml

  <fileSets>
    <fileSet filtered="true" packaged="true">
      <directory>src/main/java</directory>
    </fileSet>
    <fileSet filtered="true" packaged="true">
      <directory>src/main/resources</directory>
    </fileSet>
    <fileSet filtered="true" packaged="false">
      <directory>src/main/scripts</directory>
    </fileSet>
    <fileSet filtered="true" packaged="true">
      <directory>src/test/java</directory>
    </fileSet>
    <fileSet filtered="true" packaged="false">
       <directory>src/main/unpackaged-resources</directory>
    </fileSet>
    <fileSet filtered="true" packaged="true">
      <directory>src/main/filtered</directory>
    </fileSet>
  </fileSets>

Note that the archetype generation system is only capable of renaming a directory in one sense. If you set packaged="true" the system will install files into a package under the directory. That is, if the package of your generated project is com.example.myProject, then the contents of src/test/java get copied to src/test/java/com/example/myProject. This is what you want for Java source code, but this is unacceptable for the log4j.properties file, which will only get found by Log4J if it is installed in the root of the classpath. I would have liked to have been able to just stick this in the resources directory, but I couldn't do that, because it would have been relocated. Instead, I put it in the unpackaged-resources directory of the generated project and rely on a cantrip in the generated POM (described later) to have it installed in the right place.

Some answers to interesting problems that come up can be found in the src/main/resources/archetype-resources/src/main/scripts/path.sh file, which looks like

#!/bin/sh
#set( $symbol_dollar = '$' )
#set( $slashedGroup = $groupId.replace(".","/") )
alias ${artifactId}="java -jar $HOME/.m2/repository/${slashedGroup}/${artifactId}/${symbol_dollar}{project.version}/${artifactId}-${symbol_dollar}{project.version}-onejar.jar"

we define the {$symbol_dollar} symbol so that we can escape ${project.version} so it will be properly filtered in the last step. The definition of $slashedGroup solves a problem that turns up a lot with this sort of system, which is turning a package path into a directory path. A definition that works for Windows is in the corresponding path.ps1 file, and looks like

#set( $slashedGroup = $groupId.replace('.','\\') )

Filtering during generated project build

Finally some filtering happens when you do mvn install on the generated project.

This is controlled by the following code in the template POM:

    <resources>
      <resource>
        <filtering>true</filtering>
        <directory>\${basedir}/src/main/filtered</directory>
      </resource>
      <resource>
        <filtering>false</filtering>
        <directory>\${basedir}/src/main/resources</directory>
      </resource>
      <resource>
        <filtering>false</filtering>
        <directory>\${basedir}/src/main/unpackaged-resources</directory>
      </resource>
      <resource>
        <filtering>true</filtering>
        <targetPath>..</targetPath>
        <directory>\${basedir}/src/main/scripts</directory>
      </resource>
    </resources>

The src\main\filtered directory contains a special properties file which is used to encode the version number of the package into the JAR:

#set( $symbol_dollar = '$' )
${package}.version=${symbol_dollar}{project.version}

Note that in the above set of processing, we substitute in the ${package} name. (Since the directory path is hard-wired, we might as well hard wire the package name.) This gets rewritten at archetype generation time to

com.example.myProject.version=${project.version}

which in turn picks up the version number when you build the generated project. Note that the configuration in the POM above causes the output of filtered, resources, and unpackaged-resources to all be copied to the classpath, which merges the version.properties file and the log4j.properties with all of the other resource files when you really build your project.

The above configuration also causes the scripts, after the last round of filtering, to be written directly into the target directory and not be packaged up in the JAR.