Skip to content

Instantly share code, notes, and snippets.

@hfs
Created March 7, 2013 10:47
Show Gist options
  • Save hfs/5107205 to your computer and use it in GitHub Desktop.
Save hfs/5107205 to your computer and use it in GitHub Desktop.
Ant regexp mapper to mass rename/move files, reading its regular expressions from a file. Public Domain code, do with it whatever you like.
package com.github.hfs.anttasks;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.util.CompositeMapper;
import org.apache.tools.ant.util.FileNameMapper;
import org.apache.tools.ant.util.RegexpPatternMapper;
/**
* <p>
* A <code>FileNameMapper</code> that maps using a collection of regular
* expressions that are read from a file.
* </p>
* <p>
* The regular expressions are read from a text file. Each line contains two
* expressions with the <em>from</em> and the <em>to</em> pattern. A delimiter
* character is defined as the first character of the line and must be used to
* separate the two expressions and finish the second. Each file name to be
* mapped is matched against all <em>from</em> expressions and for all matches
* the <em>to</em> expressions are evaluated and returned.
* </p>
* <p>
* The <em>to</em> expression defines the complete target file name including
* the directory part, so if any of that should be preserved, work with grouping
* <code>(...)</code> and backreferences <code>\1, \2, </code>... This is the
* same behaviour as in Ant's {@code <regexpmapper>}.
* </p>
* <p>
* A backslash "<code>\</code>" can be used to escape the delimiter character.
* All other backslashes are used in the regular expressions and don't need to
* be escaped themselves.
* </p>
* <p>
* An example definition file could be:
* <pre>
* {@code
* ,(.*)from\.(.*),\1to\,name\2,
* }
* </pre>
* Here "<code>,</code>" is the delimiter.
* </p>
* <p>
* Internally a {@code CompositeMapper} with {@code RegexpPatternMapper}s which
* are initialized from the file's contents are used.
* </p>
* <p>
* To use the mapper efficiently typedef it to a name:
* <pre>{@code
* <typedef name="regexpfilemapper"
* classname="com.github.hfs.anttasks.AntRegexpFileMapper">
* <classpath .../>
* </typedef>
* }</pre>
* It can then be used like the other mappers:
* <pre>{@code
* <copy todir="to">
* <fileset dir="from"/>
* <regexpfilemapper file="regexp.properties" handleDirSep="true"/>
* </copy>
* }</pre>
* </p>
*
* @author <a href="mailto:hfs@gmx.de">Hermann Schwarting</a>
*/
public class AntRegexpFileMapper implements FileNameMapper {
/** Escape character in pattern definitions */
private static final char ESCAPE = '\\';
/** Holds all used RegexpPatternMappers */
private final CompositeMapper mappers = new CompositeMapper();
/** Fail if the file does not exist or cannot be read. Default: {@value} */
boolean mandatory = true;
/** The file to read expressions from */
File file;
/**
* Expressions should be matched case sensitive. Default: {@value} . Passed
* through to all RegexpPatternMappers
*/
boolean caseSensitive = true;
/**
* Treat a "\" character in a filename as a "/" for the purposes of
* matching. Default: {@value} . Passed through to all RegexpPatternMappers.
*/
boolean handleDirSep = false;
/** The input file has been read and all mappers have been setup. */
boolean setupComplete = false;
@Override
public String[] mapFileName(String sourceFileName) {
if (! setupComplete) {
validate();
readRegexpFile();
setupComplete = true;
}
return mappers.mapFileName(sourceFileName);
}
/** The file to read expressions from */
public void setFile(File regexpFile) {
this.file = regexpFile;
}
/** Fail if the file does not exist or cannot be read. */
public void setMandatory(boolean mandatory) {
this.mandatory = mandatory;
}
/** Expressions should be matched case sensitive */
public void setCaseSensitive(boolean caseSensitive) {
this.caseSensitive = caseSensitive;
}
/**
* Treat a "\" character in a filename as a "/" for the purposes of
* matching.
*/
public void setHandleDirSep(boolean handleDirSep) {
this.handleDirSep = handleDirSep;
}
/**
* Read the file with regexp pattern definitions and create the
* {@code RegexpPatternMapper}s that will do the actual mapping.
*
* @throws BuildException if {@code mandatory} is {@code true} and the file
* does not exist or an IO error happens.
*/
private void readRegexpFile() throws BuildException {
try {
BufferedReader reader = new BufferedReader(new FileReader(this.file));
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) {
continue;
}
ReplaceRegexps fromTo = parseRegexps(line);
RegexpPatternMapper mapper = new RegexpPatternMapper();
mapper.setFrom(fromTo.getFrom());
mapper.setTo(fromTo.getTo());
mapper.setCaseSensitive(caseSensitive);
mapper.setHandleDirSep(handleDirSep);
mappers.add(mapper);
}
} catch (FileNotFoundException e) {
if (mandatory) {
throw new BuildException(e);
}
} catch (IOException e) {
if (mandatory) {
throw new BuildException(e);
}
}
}
/**
* Split one line of input into the <em>from</em> and the <em>to</em>
* pattern. The lines must be of the form "|from|to|" where "|" is a
* user-defined delimiter character.
*
* @param line One non-empty line of input
* @return the split <em>from</em> and <em>to</em> expressions
* @throws BuildException if the line is not well-formed
*/
private ReplaceRegexps parseRegexps(String line) {
StringBuffer from = new StringBuffer();
StringBuffer to = new StringBuffer();
boolean escape = false;
// 0 = from, 1 = to, 2 = trailing
int part = 0;
int i;
char delimiter = line.charAt(0);
if (Character.isWhitespace(delimiter) || delimiter == ESCAPE) {
throw new BuildException("Whitespace or '" + ESCAPE + "' are not " +
"allowed as delimiter in line '" + line + "'");
}
for (i = 1; i < line.length(); ++i) {
boolean add = true;
char c = line.charAt(i);
if (escape) {
escape = false;
} else if (c == ESCAPE) {
// Escape mode "on" if the next is the delimiter
if (i < line.length() - 1 && line.charAt(i + 1) == delimiter) {
escape = true;
add = false; // Swallow the escape char
}
} else if (c == delimiter) {
++part;
add = false;
}
if (add) {
if (part == 0) {
from.append(c);
} else if (part == 1) {
to.append(c);
} else {
throw new BuildException("Trailing characters in regexp " +
"definition in line '" + line + "'");
}
}
}
if (part < 2) {
throw new BuildException("Incomplete mapping definition in line '" +
line + "'");
}
return new ReplaceRegexps(from.toString(), to.toString());
}
/**
* Check if all mandatory attributes have been set.
*
* @throws BuildException if a mandatory attribute is not set
*/
private void validate() throws BuildException {
if (mandatory && file == null) {
throw new BuildException("Mandatory attribute 'file' not set");
}
}
/** Ignored */
@Override
public void setFrom(String ignore) {
}
/** Ignored */
@Override
public void setTo(String ignore) {
}
/**
* Replacement expressions <em>from</em> and <em>to</em>.
*/
private class ReplaceRegexps {
private final String from;
private final String to;
public ReplaceRegexps(String from, String to) {
this.from = from;
this.to = to;
}
public String getFrom() {
return from;
}
public String getTo() {
return to;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment