Links

  • 1. Sogeti
  • 2. JBoss
  • 3. IBM
  • 4. Oracle
  • 5. SpringSource
  • 6. NL-JUG
  • 7. Java

Archives

Syndication  RSS 2.0

RSS 1.0
RSS 2.0

Bookmark this site

Add 'JCN Blog' site to delicious  Add 'JCN Blog' site to technorati  Add 'JCN Blog' site to digg  Add 'JCN Blog' site to dzone

Posted by Barend Garvelink at 23:08 on Thursday 28 August    Add 'maven-tstamp-plugin' site to delicious  Add 'maven-tstamp-plugin' site to technorati  Add 'maven-tstamp-plugin' site to digg  Add 'maven-tstamp-plugin' site to dzone

Since (almost) the dawn of time, ant has had the <tstamp/> task to define build date and time properties in your build for filtering resource files. Maven has no such thing. They do have a workaround which requires a temporary file on the filesystem. A google search on "maven bulid date" takes you to the maven-buildnumber-plugin, which I’m sure works fine but it does more than I need and a few things I don’t need (like access the SCM system). So I wrote a quick maven-tstamp-plugin to match the ant task. The configuration is a bit more verbose, but simple to understand.

Here’s a sample config…

<plugin>
  <groupId>nl.sogeti.jcn.maven.plugins</groupId>
  <artifactId>maven-tstamp-plugin</artifactId>
  <version>1.0</version>
  <configuration>
    <!-- All properties prefixed with 'build.' -->
    <prefix>build.</prefix>
    
    <!-- UNIX timestamp published as 'build.unix-timestamp' -->
    <timestamp>unix-timestamp</timestamp>
    <properties>
      <!-- Publishes build.sortableDate, default locale, default timezone -->
      <sortableDate>yyyy-MM-dd</sortableDate>
      
      <!-- Publishes build.prettyDate, Argentina's locale and timezone -->
      <prettyDate>EEEE d MMMM yyyy, H:mm</prettyDate>
      <prettyDate.locale>es_AR</prettyDate.locale>
      <prettyDate.zone>America/Argentina/Buenos_Aires</prettyDate.zone>
      
      <!-- Publishes build.utc-stamp (calendar-based timestamp) in UTC. -->
      <utc-stamp>yyyyMMddHHmmss</utc-stamp>
      <utc-stamp.zone>UTC</utc-stamp.zone>
    </properties>
  </configuration>
</plugin>

…which I think speaks for itself. Each of the properties defined can be used in ${filtering} expressions. I didn’t require the offset/unit feature in the original <tstamp/> task, nor did I require Joda-Time’s support for nonwestern calendar systems. I’ve reserved suffixed for both, but they currently just cause the property to be ignored.

Source code after the jump.

pom.xml

<!--
 * Copyright 2008 Sogeti Nederland B.V.
 * 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.
-->
<project>
 <modelVersion>4.0.0</modelVersion>
 <groupId>nl.sogeti.jcn.maven.plugins</groupId>
 <artifactId>maven-tstamp-plugin</artifactId>
 <packaging>maven-plugin</packaging>
 <version>1.0</version>
 <name>maven-tstamp-plugin</name>
 <description><![CDATA[A maven plugin to emulate ant s <stamp/> task]]></description>
 <dependencies>
   <dependency>
     <groupId>org.apache.maven</groupId>
     <artifactId>maven-plugin-api</artifactId>
     <version>2.0</version>
   </dependency>
   <dependency>
     <groupId>org.apache.maven</groupId>
     <artifactId>maven-project</artifactId>
     <version>2.0</version>
   </dependency>
   <dependency>
     <groupId>joda-time</groupId>
     <artifactId>joda-time</artifactId>
     <version>1.5.2</version>
   </dependency>
 </dependencies>
 <build>
  <plugins>
    <plugin>
     <groupId>org.apache.maven.plugins</groupId>
     <artifactId>maven-compiler-plugin</artifactId>
     <configuration>
      <source>1.5</source>
      <target>1.5</target>
     </configuration>
    </plugin>
  </plugins>
 </build>
</project>

src/main/java/nl/sogeti/jcn/maven/plugins/tstamp/TstampMojo.java

/*
 * Copyright 2008 Sogeti Nederland B.V.
 * 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.
 */
package nl.sogeti.jcn.maven.plugins.tstamp;

import java.util.Locale;
import java.util.Map;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

/**
 * Emulates ant's humble <tstamp/> task.
 *
 * Sample configuration:
 * <![CDATA[
 * <!-- in the /project/build/plugins pom section -->
 * <plugin>
 *   <groupId>nl.sogeti.jcn.maven.plugins</groupId>
 *   <artifactId>maven-tstamp-plugin</artifactId>
 *   <version>1.0</version>
 *   <configuration>
 *     <!-- All properties prefixed with 'build.' -->
 *     <prefix>build.</prefix>
 *
 *     <!-- UNIX timestamp published as 'build.unix-timestamp' -->
 *     <timestamp>unix-timestamp</timestamp>
 *
 *     <properties>
 *       <!-- Publishes build.sortableDate, default locale, default timezone -->
 *       <sortableDate>yyyy-MM-dd</sortableDate>
 *
 *       <!-- Publishes build.prettyDate, Argentina's locale and timezone -->
 *       <prettyDate>EEEE d MMMM yyyy, H:mm</prettyDate>
 *       <prettyDate.locale>es_AR</prettyDate.locale>
 *       <prettyDate.zone>America/Argentina/Buenos_Aires</prettyDate.zone>
 *
 *       <!-- Publishes build.utc-stamp, default locale, UTC timezone -->
 *       <utc-stamp>yyyyMMddHHmmss</utc-stamp>
 *       <utc-stamp.zone>UTC</utc-stamp.zone>
 *     </properties>
 *   </configuration>
 * </plugin>
 * ]]>
 * Note: if the plugin isn't executed, you may need to define an execution:
 * <![CDATA[
 * <!-- in the /project/build/plugins pom section -->
 * <plugin>
 *   <groupId>nl.sogeti.jcn.maven.plugins</groupId>
 *   <artifactId>maven-tstamp-plugin</artifactId>
 *   <version>1.0</version>
 *   <executions>
 *     <execution>
 *       <phase>validate</phase>
 *       <goals>
 *         <goal>tstamp</goal>
 *       </goals>
 *       <configuration>
 *         <!-- see previous example -->
 *       </configuration>
 *     </execution>
 *   </executions>
 * </plugin>
 * ]]>
 *
 * @goal tstamp
 * @phase verify
 * @requiresProject true
 * @requiresOnline false
 * @author Barend Garvelink, Sogeti Nederland B.V.
 */
@SuppressWarnings("unchecked")
public class TstampMojo extends AbstractMojo
{
  /**
   * If set, this value is prepended to all property names defined in the
   * "timestamp" and "properties" elements. It is recommended, but not required,
   * that this ends in a dot.
   * @parameter
   */
  private String prefix;

  /**
   * This optional parameter names the build property by which the UNIX
   * timestamp is made available.
   * @parameter
   */
  private String timestamp;

  /**
   * This optional parameter configures which formatted date/time properties are
   * made available, and how.
   *
   * - Each key defines a property name, its value is a format string acceptable
   *   to Joda-Time's DateTimeFormat class [1]. If the value is not a valid
   *   pattern, the build fails.
   * - If a key ends in '.zone', it defines the timezone for the property of the
   *   same name. This must be a timezone ID acceptable to Joda-Time's
   *   DateTimeZone [2]. If the property is defined, this entry is ignored. If
   *   no '.zone' property is set, the default time zone is used. If the value
   *   is not a known timezone ID, the build fails.
   * - If a key ends in '.locale', it defines the locale for the property of the
   *   same name. The value is split on underscores. The first element is inter-
   *   preted as the language, the second part as the country, and the third
   *   part as the variant. Any subsequent parts are ignored. If the requested
   *   locale is unknown, java.util.Locale constructs the system default locale.
   * - Keys ending in '.offset', '.unit' and '.chrono' are ignored. These
   *   suffixes are reserved for full compatibility with ant's tstamp task and
   *   for exploiting Joda-Time's support for nonwestern calendar systems.
   *
   * [1] http://joda-time.sourceforge.net/api-release/
   *                                    org/joda/time/format/DateTimeFormat.html
   * [2] http://joda-time.sourceforge.net/api-release/
   *                                    org/joda/time/DateTimeZone.html
   *
   * @parameter
   */
  private Map properties;

  /**
   * This is an internal variable automatically set by Maven
   * @parameter expression="${project}"
   * @readonly
   * @required
   */
  private MavenProject project;

  /**
   * {@inheritDoc}
   */
  public void execute() throws MojoExecutionException
  {
    Log logger = getLog();

    final DateTime now = new DateTime();
    final DateTimeZone defaultTz = now.getZone();
    final Locale defaultLocale = Locale.getDefault();

    if (logger.isDebugEnabled()) {
      logger.debug(String.format(
          "tstamp mojo starting. Actual configuration: prefix=\"%s\", timestamp=\"%s\", properties=%s. System defaults: timezone=%s, locale=%s.",
          prefix, timestamp, properties, defaultTz, defaultLocale));
    }

    if (timestamp == null && (properties == null || properties.isEmpty())) {
      logger.warn("You have defined neither the \"timestamp\", nor the \"properties\" configuration element. This plugin has no effect."
          +" For help configuring this plugin, run `mvn help:describe -DgroupId=nl.sogeti.jcn.maven.plugins -DartifactId=maven-tstamp-plugin -Dversion=1.0 -Dmedium=true`"
          +" on your command line.");
      return;
    }

    if (properties != null) {
      Map<String, String> props = (Map<String,String>)properties;
      for (Map.Entry<String, String> entry : props.entrySet()) {
        final String key = entry.getKey();
        if (isSpecialKey(key)) {
          continue;
        }

        DateTimeFormatter format = parsePattern(entry.getValue());
        DateTimeZone zone = getTimezone(props.get(key + ".zone"));
        Locale locale = getLocale(props.get(key + ".locale"));

        DateTime printMe = now;
        if (zone != null) {
          printMe = now.withZone(zone);
        }

        if (locale != null) {
          format = format.withLocale(locale);
        }

        final String value = format.print(printMe);
        putProperty(key, value);
      }
    }

    if (timestamp != null) {
      putProperty(timestamp, Long.toString(now.getMillis()));
    }
  }

  /**
   * Parses the given string as a locale identifier. It is split on underscores
   * and the first 1-3 elements are used, in order, as the constructor params
   * for java.util.Locale. If the entire input is <code>null</code>, this method
   * returns null.
   *
   * Note that <code>java.util.Locale</code>'s constructor does not fail when
   * invalid inputs are passed. Instead, it returns the system default locale.
   *
   * @param id the locale id.
   * @return the locale constructed from the input argument.
   */
  private Locale getLocale(String id) {
    if (id != null) {
      String[] elems = id.split("_");
      if (elems.length == 1) {
        return new Locale(elems[0]);
      }
      if (elems.length == 2) {
        return new Locale(elems[0], elems[1]);
      }
      if (elems.length >= 3) {
        return new Locale(elems[0], elems[1], elems[2]);
      }
    }
    return null;
  }

  /**
   * Interprets the given string as a timezone ID or triggers a build failure
   * with a pretty error message if the input isn't a valid id.
   * @param id the string to parse as a timezone id.
   * @return the timezone found.
   * @throws MojoExecutionException if the input is non-<code>null</code> and
   *  invalid as a timezone id.
   */
  private DateTimeZone getTimezone(String id) throws MojoExecutionException {
    if (id != null) {
      try {
        return DateTimeZone.forID(id);
      }
      catch(IllegalArgumentException e) {
        throw new MojoExecutionException(String.format(
            "Value \"%s\" is not a known timezone ID", id));
      }
    }
    return null;
  }

  /**
   * Constructs a DateTimeFormatter from the given pattern string or triggers a
   * build failure if the input isn't a valid pattern.
   * @param pattern the pattern string.
   * @return a formatter for the given pattern.
   * @throws MojoExecutionException if the pattern is invalid.
   */
  private DateTimeFormatter parsePattern(String pattern)
    throws MojoExecutionException {
    try {
      return DateTimeFormat.forPattern(pattern);
    }
    catch (IllegalArgumentException e) {
      throw new MojoExecutionException(String.format(
          "Value \"%s\" is not a valid formatting pattern (%s).",
          pattern, e.getMessage()));
    }
  }

  /**
   * Returns true if the given property key is a special key used to modify
   * another and shouldn't be published as a property.
   * @param key a property key.
   * @return <code>true</code> if this is a special key.
   */
  private boolean isSpecialKey(String key) {
    return key.endsWith(".zone")
        || key.endsWith(".locale")
        || key.endsWith(".offset")
        || key.endsWith(".unit")
        || key.endsWith(".chrono");
  }

  /**
   * Adds the given key and value to the property map.
   *
   * @param key the key.
   * @param value the value.
   */
  private void putProperty(String key, String value) {
    final String realKey = makeKey(key);
    getLog().info(String.format("  %s => %s", realKey, value));
    if (project.getProperties().put(realKey, value) != null) {
      getLog().warn(String.format(
          "Property key \"%s\" replaced an existing entry.", key));
    }
  }

  /**
   * Applies the value of {@link #prefix} to the given property name if {@code prefix} is non-null.
   *
   * @param name the property name.
   * @return if prefix is <code>null</code>, returns <code>name</code>. Otherwise,
   * returns <code>prefix + name</code>.
   */
  private String makeKey(String name) {
    if (prefix == null) {
      return name;
    }
    return prefix + name;
  }

  public String toString() {
    return "maven-tstamp-plugin TstampMojo";
  }

  /**
   * Standard getter for {@code prefix}.
   * @return the current value of {@code prefix}.
   * @category plumbing
   */
  public String getPrefix() {
    return prefix;
  }

  /**
   * Standard setter for {@code prefix}.
   * @param prefix the new value for {@code prefix}.
   * @category plumbing
   */
  public void setPrefix(String prefix) {
    this.prefix = prefix;
  }

  /**
   * Standard getter for {@code timestamp}.
   * @return the current value of {@code timestamp}.
   * @category plumbing
   */
  public String getTimestamp() {
    return timestamp;
  }

  /**
   * Standard setter for {@code timestamp}.
   * @param timestamp the new value for {@code timestamp}.
   * @category plumbing
   */
  public void setTimestamp(String timestamp) {
    this.timestamp = timestamp;
  }

  /**
   * Standard getter for {@code properties}.
   * @return the current value of {@code properties}.
   * @category plumbing
   */
  public Map getProperties() {
    return properties;
  }

  /**
   * Standard setter for {@code properties}.
   * @param properties the new value for {@code properties}.
   * @category plumbing
   */
  public void setProperties(Map properties) {
    this.properties = properties;
  }

  /**
   * Standard getter for {@code project}.
   * @return the current value of {@code project}.
   * @category plumbing
   */
  public MavenProject getProject() {
    return project;
  }

  /**
   * Standard setter for {@code project}.
   * @param project the new value for {@code project}.
   * @category plumbing
   */
  public void setProject(MavenProject project) {
    this.project = project;
  }
}

© 2014 Java Competence Network. All Rights Reserved.