Posted by Barend Garvelink at 23:08 on Thursday 28 August
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;
}
}











