Ant Constants Class Generator Task — Step-by-Step Implementation Guide
This guide walks through creating an Ant task that generates a Java constants class from configuration (properties/XML). It explains goals, design, implementation, and usage with code examples so you can integrate the task into a build pipeline.
Goal
Create a custom Apache Ant task named AntConstantsGeneratorTask that:
- Reads key/value pairs from a properties file (or XML).
- Generates a Java class containing public static final constants for each key.
- Supports type inference for common types (String, int, long, boolean, double).
- Allows package name, class name, target directory, and optional prefix/suffix configuration.
- Skips overwriting if content would be identical (avoid unnecessary recompilation).
- Integrates cleanly into Ant builds.
Design overview
Inputs
- propertiesFile (required): path to the .properties file (or XML source).
- packageName (optional): Java package for the generated class.
- className (required): name of the generated Java class.
- targetDir (required): directory where the .java file will be written.
- prefix / suffix (optional): add to constant names.
- accessModifier (optional): public or package-private (default public).
- generateComments (optional): include comments and generation timestamp.
- overwrite (optional): force overwrite even if unchanged (default false).
- encoding (optional): file encoding (default UTF-8).
Output
- One Java source file at targetDir/[package path]/className.java.
Type inference rules (simple)
- “true”/“false” → boolean
- Integer parse succeeds → int
- Long parse (ends with L or too big for int) → long
- Double parse succeeds → double
- Otherwise → String
Constant naming
- Transform property keys to UPPER_SNAKECASE:
- Replace non-alphanumeric with underscore
- Collapse multiple underscores
- Trim leading/trailing underscores
- Apply prefix/suffix if provided
Implementation
1) Create the Ant task class
Create a Java class extending org.apache.tools.ant.Task. Below is a concise implementation.
java
package com.example.ant; import org.apache.tools.ant.Task; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; import java.io.; import java.nio.charset.StandardCharsets; import java.nio.file.; import java.util.; import java.util.regex.; public class AntConstantsGeneratorTask extends Task { private File propertiesFile; private String packageName = ””; private String className; private File targetDir; private String prefix = ””; private String suffix = ””; private boolean overwrite = false; private boolean generateComments = true; private String encoding = “UTF-8”; public void setPropertiesFile(File propertiesFile) { this.propertiesFile = propertiesFile; } public void setPackageName(String packageName) { this.packageName = packageName == null ? ”” : packageName; } public void setClassName(String className) { this.className = className; } public void setTargetDir(File targetDir) { this.targetDir = targetDir; } public void setPrefix(String prefix) { this.prefix = prefix == null ? ”” : prefix; } public void setSuffix(String suffix) { this.suffix = suffix == null ? ”” : suffix; } public void setOverwrite(boolean overwrite) { this.overwrite = overwrite; } public void setGenerateComments(boolean generateComments) { this.generateComments = generateComments; } public void setEncoding(String encoding) { this.encoding = encoding == null ? “UTF-8” : encoding; } @Override public void execute() throws BuildException { validate(); Properties props = loadProperties(); String source = generateSource(props); writeSourceIfChanged(source); } private void validate() { if (propertiesFile == null) throw new BuildException(“propertiesFile is required”); if (!propertiesFile.exists()) throw new BuildException(“propertiesFile not found: “ + propertiesFile); if (className == null || className.trim().isEmpty()) throw new BuildException(“className is required”); if (targetDir == null) throw new BuildException(“targetDir is required”); } private Properties loadProperties() { Properties p = new Properties(); try (InputStream in = new FileInputStream(propertiesFile); Reader r = new InputStreamReader(in, encoding)) { p.load(r); } catch (IOException e) { throw new BuildException(“Failed to load properties: “ + e.getMessage(), e); } return p; } private String generateSource(Properties props) { StringBuilder sb = new StringBuilder(); if (!packageName.isEmpty()) { sb.append(“package “).append(packageName).append(”;“
); } if (generateComments) { sb.append(”/**) .append(new Date()).append(” */ “); } sb.append(“public final class “).append(className).append(” {
- Generated by AntConstantsGeneratorTask on “
“
); sb.append(” private “).append(className).append(”() {}“
); List<String> keys = new ArrayList<>(); for (Object k : props.keySet()) keys.add(k.toString()); Collections.sort(keys); for (String key : keys) { String raw = props.getProperty(key); String constName = makeConstantName(key); TypeAndValue tv = inferType(raw); sb.append(” public static final “) .append(tv.type).append(” “).append(constName).append(” = “) .append(tv.literal).append(”; “); } sb.append(”} “); return sb.toString(); } private static class TypeAndValue { final String type; final String literal; TypeAndValue(String t, String l) { type = t; literal = l; } } private TypeAndValue inferType(String raw) { if (raw == null) raw = ””; String trimmed = raw.trim(); if (“true”.equalsIgnoreCase(trimmed) || “false”.equalsIgnoreCase(trimmed)) { return new TypeAndValue(“boolean”, trimmed.toLowerCase()); } try { int i = Integer.parseInt(trimmed); return new TypeAndValue(“int”, Integer.toString(i)); } catch (Exception ignored) {} try { long L = Long.parseLong(trimmed); return new TypeAndValue(“long”, Long.toString(L) + “L”); } catch (Exception ignored) {} try { double d = Double.parseDouble(trimmed); if (!Double.isInfinite(d) && !Double.isNaN(d)) { return new TypeAndValue(“double”, Double.toString(d)); } } catch (Exception ignored) {} // Fallback to String with escaped characters String escaped = trimmed.replace(”\”, “\”).replace(”“”, ”\“”); return new TypeAndValue(“String”, ”“” + escaped + ”“”); } private String makeConstantName(String key) { String s = key.replaceAll(”[^A-Za-z0-9]+”, “”); s = s.replaceAll(“+”, “”); s = s.replaceAll(”^+|+$”, ””); s = s.toUpperCase(Locale.ROOT); if (!prefix.isEmpty()) s = prefix.toUpperCase(Locale.ROOT) + “” + s; if (!suffix.isEmpty()) s = s + “” + suffix.toUpperCase(Locale.ROOT); if (s.isEmpty()) s = “CONST”; if (Character.isDigit(s.charAt(0))) s = “” + s; return s; } private void writeSourceIfChanged(String content) { Path outDir = targetDir.toPath(); if (!packageName.isEmpty()) outDir = outDir.resolve(packageName.replace(’.’, File.separatorChar)); Path outFile = outDir.resolve(className + ”.java”); try { Files.createDirectories(outDir); byte[] newBytes = content.getBytes(encoding); if (Files.exists(outFile) && !overwrite) { byte[] oldBytes = Files.readAllBytes(outFile); if (Arrays.equals(oldBytes, newBytes)) { log(“No change detected; skipping write for “ + outFile, Project.MSG_VERBOSE); return; } } Files.write(outFile, newBytes); log(“Wrote constants class to “ + outFile, Project.MSGINFO); } catch (IOException e) { throw new BuildException(“Failed to write generated source: “ + e.getMessage(), e); } } }
Packaging and antlib/taskdef
- Compile and package into a JAR with proper manifest.
- Example build.xml snippet to register the task:
xml
<taskdef name=“generateConstants” classname=“com.example.ant.AntConstantsGeneratorTask” classpath=“lib/ant-constants-generator.jar”/>
Usage example in build.xml
xml
<target name=“generate”> <generateConstants propertiesFile=“config/constants.properties” className=“BuildConstants” packageName=“com.myapp.config” targetDir=“${src.generated}” prefix=“APP” overwrite=“false” generateComments=“true”/> </target>
Testing and edge cases
- Keys with illegal Java identifier start characters are prefixed with underscore.
- Very large numbers may be emitted as long/double; review overflow risk.
- Binary or multi-line property values treated as strings; consider escaping newlines if needed.
- For XML input: add alternate loader that parses elements/attributes.
Enhancements
- Support for annotations (e.g., @Generated).
- Option to generate Kotlin/Scala objects.
- Support for nested classes grouping by key prefix.
- Allow custom type mappings via a mapping file.
Summary
This implementation provides a practical Ant task to generate a Java constants class from properties with type inference, safe writes, and configurable naming. Drop the compiled task JAR into your build classpath, register with taskdef, and call from your Ant targets to keep generated constants synchronized with configuration.
Leave a Reply