blob: 54828f8598817c7d309db0a2ffa8c0026f424980 [file] [log] [blame]
/**
*******************************************************************************
* Copyright (C) 2004-2012, International Business Machines Corporation and *
* others. All Rights Reserved. *
*******************************************************************************
*/
package com.ibm.icu.dev.tool.docs;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
/**
* A simple facility for adding C-like preprocessing to .java files.
* This only understands a subset of the C preprocessing syntax.
* Its used to manage files that with only small differences can be
* compiled for different JVMs. This changes files in place,
* commenting out lines based on the current flag settings.
*/
public class CodeMangler {
private File indir; // root of input
private File outdir; // root of output
private String suffix; // suffix to process, default '.jpp'
private boolean recurse; // true if recurse on directories
private boolean force; // true if force reprocess of files
private boolean clean; // true if output is to be cleaned
private boolean timestamp; // true if we read/write timestamp
private boolean nonames; // true if no names in header
private HashMap map; // defines
private ArrayList names; // files/directories to process
private String header; // sorted list of defines passed in
private boolean verbose; // true if we emit debug output
private static final String IGNORE_PREFIX = "//##";
private static final String HEADER_PREFIX = "//##header";
public static void main(String[] args) {
new CodeMangler(args).run();
}
private static final String usage = "Usage:\n" +
" CodeMangler [flags] file... dir... @argfile... \n" +
"-in[dir] path - root directory of input files, otherwise use current directory\n" +
"-out[dir] path - root directory of output files, otherwise use input directory\n" +
"-s[uffix] string - suffix of inputfiles to process, otherwise use '.java' (directories only)\n" +
"-c[lean] - remove all control flags from code on output (does not proceed if overwriting)\n" +
"-r[ecurse] - if present, recursively process subdirectories\n" +
"-f[orce] - force reprocessing of files even if timestamp and headers match\n" +
"-t[imestamp] - expect/write timestamp in header\n" +
"-dNAME[=VALUE] - define NAME with optional value VALUE\n" +
" (or -d NAME[=VALUE])\n" +
"-n - do not put NAME/VALUE in header\n" +
"-help - print this usage message and exit.\n" +
"\n" +
"For file arguments, output '.java' files using the same path/name under the output directory.\n" +
"For directory arguments, process all files with the defined suffix in the directory.\n" +
" (if recursing, do the same for all files recursively under each directory)\n" +
"For @argfile arguments, read the specified text file (strip the '@'), and process each line of that file as \n" +
"an argument.\n" +
"\n" +
"Directives are one of the following:\n" +
" #ifdef, #ifndef, #else, #endif, #if, #elif, #define, #undef\n" +
"These may optionally be preceeded by whitespace or //.\n" +
"#if, #elif args are of the form 'key == value' or 'key != value'.\n" +
"Only exact character match key with value is performed.\n" +
"#define args are 'key [==] value', the '==' is optional.\n";
CodeMangler(String[] args) {
map = new HashMap();
names = new ArrayList();
suffix = ".java";
clean = false;
timestamp = false;
String inname = null;
String outname = null;
boolean processArgs = true;
String arg = null;
try {
for (int i = 0; i < args.length; ++i) {
arg = args[i];
if ("--".equals(arg)) {
processArgs = false;
} else if (processArgs && arg.charAt(0) == '-') {
if (arg.startsWith("-in")) {
inname = args[++i];
} else if (arg.startsWith("-out")) {
outname = args[++i];
} else if (arg.startsWith("-d")) {
String id = arg.substring(2);
if (id.length() == 0) {
id = args[++i];
}
String val = "";
int ix = id.indexOf('=');
if (ix >= 0) {
val = id.substring(ix+1);
id = id.substring(0,ix);
}
map.put(id, val);
} else if (arg.startsWith("-s")) {
suffix = args[++i];
} else if (arg.startsWith("-r")) {
recurse = true;
} else if (arg.startsWith("-f")) {
force = true;
} else if (arg.startsWith("-c")) {
clean = true;
} else if (arg.startsWith("-t")) {
timestamp = true;
} else if (arg.startsWith("-h")) {
System.out.print(usage);
break; // stop before processing arguments, so we will do nothing
} else if (arg.startsWith("-v")) {
verbose = true;
} else if (arg.startsWith("-n")) {
nonames = true;
} else {
System.err.println("Error: unrecognized argument '" + arg + "'");
System.err.println(usage);
throw new IllegalArgumentException(arg);
}
} else {
if (arg.charAt(0) == '@') {
File argfile = new File(arg.substring(1));
if (argfile.exists() && !argfile.isDirectory()) {
BufferedReader br = null;
try {
br = new BufferedReader(new InputStreamReader(new FileInputStream(argfile)));
ArrayList list = new ArrayList();
for (int x = 0; x < args.length; ++x) {
list.add(args[x]);
}
String line;
while (null != (line = br.readLine())) {
line = line.trim();
if (line.length() > 0 && line.charAt(0) != '#') {
if (verbose) System.out.println("adding argument: " + line);
list.add(line);
}
}
args = (String[])list.toArray(new String[list.size()]);
}
catch (IOException e) {
System.err.println("error reading arg file: " + e);
}
finally {
if (br != null) {
try {
br.close();
} catch (Exception e){
// ignore
}
}
}
}
} else {
names.add(arg);
}
}
}
} catch (IndexOutOfBoundsException e) {
String msg = "Error: argument '" + arg + "' missing value";
System.err.println(msg);
System.err.println(usage);
throw new IllegalArgumentException(msg);
}
String username = System.getProperty("user.dir");
if (inname == null) {
inname = username;
} else if (!(inname.startsWith("\\") || inname.startsWith("/"))) {
inname = username + File.separator + inname;
}
indir = new File(inname);
try {
indir = indir.getCanonicalFile();
}
catch (IOException e) {
// continue, but most likely we'll fail later
}
if (!indir.exists()) {
throw new IllegalArgumentException("Input directory '" + indir.getAbsolutePath() + "' does not exist.");
} else if (!indir.isDirectory()) {
throw new IllegalArgumentException("Input path '" + indir.getAbsolutePath() + "' is not a directory.");
}
if (verbose) System.out.println("indir: " + indir.getAbsolutePath());
if (outname == null) {
outname = inname;
} else if (!(outname.startsWith("\\") || outname.startsWith("/"))) {
outname = username + File.separator + outname;
}
outdir = new File(outname);
try {
outdir = outdir.getCanonicalFile();
}
catch (IOException e) {
// continue, but most likely we'll fail later
}
if (!outdir.exists()) {
throw new IllegalArgumentException("Output directory '" + outdir.getAbsolutePath() + "' does not exist.");
} else if (!outdir.isDirectory()) {
throw new IllegalArgumentException("Output path '" + outdir.getAbsolutePath() + "' is not a directory.");
}
if (verbose) System.out.println("outdir: " + outdir.getAbsolutePath());
if (clean && suffix.equals(".java")) {
try {
if (outdir.getCanonicalPath().equals(indir.getCanonicalPath())) {
throw new IllegalArgumentException("Cannot use 'clean' to overwrite .java files in same directory tree");
}
}
catch (IOException e) {
System.err.println("possible overwrite, error: " + e.getMessage());
throw new IllegalArgumentException("Cannot use 'clean' to overrwrite .java files");
}
}
if (names.isEmpty()) {
names.add(".");
}
TreeMap sort = new TreeMap(String.CASE_INSENSITIVE_ORDER);
sort.putAll(map);
Iterator iter = sort.entrySet().iterator();
StringBuffer buf = new StringBuffer();
if (!nonames) {
while (iter.hasNext()) {
Map.Entry e = (Map.Entry)iter.next();
if (buf.length() > 0) {
buf.append(", ");
}
buf.append(e.getKey());
String v = (String)e.getValue();
if (v != null && v.length() > 0) {
buf.append('=');
buf.append(v);
}
}
}
header = buf.toString();
}
public int run() {
return process("", (String[])names.toArray(new String[names.size()]));
}
public int process(String path, String[] filenames) {
if (verbose) System.out.println("path: '" + path + "'");
int count = 0;
for (int i = 0; i < filenames.length; ++i) {
if (verbose) System.out.println("name " + i + " of " + filenames.length + ": '" + filenames[i] + "'");
String name = path + filenames[i];
File fin = new File(indir, name);
try {
fin = fin.getCanonicalFile();
}
catch (IOException e) {
}
if (!fin.exists()) {
System.err.println("File " + fin.getAbsolutePath() + " does not exist.");
continue;
}
if (fin.isFile()) {
if (verbose) System.out.println("processing file: '" + fin.getAbsolutePath() + "'");
String oname;
int ix = name.lastIndexOf(".");
if (ix != -1) {
oname = name.substring(0, ix);
} else {
oname = name;
}
oname += ".java";
File fout = new File(outdir, oname);
if (processFile(fin, fout)) {
++count;
}
} else if (fin.isDirectory()) {
if (verbose) System.out.println("recursing on directory '" + fin.getAbsolutePath() + "'");
String npath = ".".equals(name) ? path : path + fin.getName() + File.separator;
count += process(npath, fin.list(filter)); // recursive call
}
}
return count;
}
private final FilenameFilter filter = new FilenameFilter() {
public boolean accept(File dir, String name) {
File f = new File(dir, name);
return (f.isFile() && name.endsWith(suffix)) || (f.isDirectory() && recurse);
}
};
public boolean processFile(File infile, File outfile) {
File backup = null;
class State {
int lc;
String line;
boolean emit = true;
boolean tripped;
private State next;
public String toString() {
return "line " + lc
+ ": '" + line
+ "' (emit: " + emit
+ " tripped: " + tripped
+ ")";
}
void trip(boolean trip) {
if (!tripped & trip) {
tripped = true;
emit = next != null ? next.emit : true;
} else {
emit = false;
}
}
State push(int lc, String line, boolean trip) {
this.lc = lc;
this.line = line;
State ret = new State();
ret.next = this;
ret.emit = this.emit & trip;
ret.tripped = trip;
return ret;
}
State pop() {
return next;
}
}
HashMap oldMap = null;
long outModTime = 0;
try {
PrintStream outstream = null;
InputStream instream = new FileInputStream(infile);
BufferedReader reader = new BufferedReader(new InputStreamReader(instream));
int lc = 0;
State state = new State();
String line;
while ((line = reader.readLine()) != null) {
if (lc == 0) { // check and write header for output file if needed
boolean hasHeader = line.startsWith(HEADER_PREFIX);
if (hasHeader && !force) {
long expectLastModified = ((infile.lastModified() + 999)/1000)*1000;
String headerline = HEADER_PREFIX;
if (header.length() > 0) {
headerline += " ";
headerline += header;
}
if (timestamp) {
headerline += " ";
headerline += String.valueOf(expectLastModified);
}
if (line.equals(headerline)) {
if (verbose) System.out.println("no changes necessary to " + infile.getCanonicalPath());
reader.close();
return false; // nothing to do
}
if (verbose) {
System.out.println(" old header: " + line);
System.out.println(" != expected: " + headerline);
}
}
// create output file directory structure
String outpname = outfile.getParent();
if (outpname != null) {
File outp = new File(outpname);
if (!(outp.exists() || outp.mkdirs())) {
System.err.println("could not create directory: '" + outpname + "'");
reader.close();
return false;
}
}
// if we're overwriting, use a temporary file
if (suffix.equals(".java")) {
backup = outfile;
try {
outfile = File.createTempFile(outfile.getName(), null, outfile.getParentFile());
}
catch (IOException ex) {
System.err.println(ex.getMessage());
reader.close();
return false;
}
}
outModTime = ((outfile.lastModified()+999)/1000)*1000; // round up
outstream = new PrintStream(new FileOutputStream(outfile));
String headerline = HEADER_PREFIX;
if (header.length() > 0) {
headerline += " ";
headerline += header;
}
if (timestamp) {
headerline += " ";
headerline += String.valueOf(outModTime);
}
outstream.println(headerline);
if (verbose) System.out.println("header: " + headerline);
// discard the old header if we had one, otherwise match this line like any other
if (hasHeader) {
++lc; // mark as having read a line so we never reexecute this block
continue;
}
}
String[] res = new String[3];
if (patMatch(line, res)) {
String lead = res[0];
String key = res[1];
String val = res[2];
if (verbose) System.out.println("directive: " + line
+ " key: '" + key
+ "' val: '" + val
+ "' " + state);
if (key.equals("ifdef")) {
state = state.push(lc, line, map.get(val) != null);
} else if (key.equals("ifndef")) {
state = state.push(lc, line, map.get(val) == null);
} else if (key.equals("else")) {
state.trip(true);
} else if (key.equals("endif")) {
state = state.pop();
} else if (key.equals("undef")) {
if (state.emit) {
if (oldMap == null) {
oldMap = (HashMap)map.clone();
}
map.remove(val);
}
} else if (key.equals("define")) {
if (pat2Match(val, res)) {
String key2 = res[0];
String val2 = res[2];
if (verbose) System.out.println("val2: '" + val2
+ "' key2: '" + key2
+ "'");
if (state.emit) {
if (oldMap == null) {
oldMap = (HashMap)map.clone();
}
map.put(key2, val2);
}
}
} else { // #if, #elif
// only top level OR (||) operator is supported for now
int count = 1;
int index = 0;
while ((index = val.indexOf("||", index)) > 0) {
count++;
index++;
}
String[] expressions = new String[count];
if (count == 1) {
expressions[0] = val;
} else {
int start = 0;
index = 0;
count = 0;
while (true) {
index = val.indexOf("||", start);
if (index > 0) {
expressions[count++] = val.substring(start, index);
start = index + 2;
} else {
expressions[count++] = val.substring(start);
break;
}
}
}
boolean eval = false;
for (count = 0; count < expressions.length && !eval; count++) {
if (pat2Match(expressions[count], res)) {
String key2 = res[0];
String val2 = res[2];
if (key2.equals("defined")) {
// defined command
if (verbose) System.out.println(
"index: '" + count
+ "' val2: '" + val2
+ "' key2: '" + key2
+ "'");
eval = map.containsKey(val2);
} else {
boolean neq = false;
if (res[1].equals("!=")) {
neq = true;
} else if (!res[1].equals("==")) {
System.err.println("Invalid expression: '" + val);
}
if (verbose) System.out.println(
"index: '" + count
+ "' val2: '" + val2
+ "' neq: '" + neq
+ "' key2: '" + key2
+ "'");
eval = (val2.equals(map.get(key2)) != neq);
}
}
}
if (key.equals("if")) {
state = state.push(lc, line, eval);
} else if (key.equals("elif")) {
state.trip(eval);
}
}
if (!clean) {
lc++;
if (!lead.equals("//")) {
outstream.print("//");
line = line.substring(lead.length());
}
outstream.println(line);
}
continue;
}
lc++;
String found = pat3Match(line);
boolean hasIgnore = found != null;
if (state.emit == hasIgnore) {
if (state.emit) {
line = line.substring(found.length());
} else {
line = IGNORE_PREFIX + line;
}
} else if (hasIgnore && !found.equals(IGNORE_PREFIX)) {
line = IGNORE_PREFIX + line.substring(found.length());
}
if (!clean || state.emit) {
outstream.println(line);
}
}
state = state.pop();
if (state != null) {
System.err.println("Error: unclosed directive(s):");
do {
System.err.println(state);
} while ((state = state.pop()) != null);
System.err.println(" in file: " + outfile.getCanonicalPath());
if (oldMap != null) {
map = oldMap;
}
reader.close();
outstream.close();
return false;
}
outstream.close();
instream.close();
if (backup != null) {
if (backup.exists()) {
backup.delete();
}
outfile.renameTo(backup);
}
if (timestamp) {
outfile.setLastModified(outModTime); // synch with timestamp
}
if (oldMap != null) {
map = oldMap;
}
}
catch (IOException e) {
System.err.println(e);
return false;
}
return true;
}
/**
* Perform same operation as matching on pat. on exit
* leadKeyValue contains the three strings lead, key, and value.
* 'lead' is the portion before the #ifdef directive. 'key' is
* the directive. 'value' is the portion after the directive. if
* there is a match, return true, else return false.
*/
static boolean patMatch(String line, String[] leadKeyValue) {
if (line.length() == 0) {
return false;
}
if (!line.endsWith("\n")) {
line = line + '\n';
}
int mark = 0;
int state = 0;
loop: for (int i = 0; i < line.length(); ++i) {
char c = line.charAt(i);
switch (state) {
case 0: // at start of line, haven't seen anything but whitespace yet
if (c == ' ' || c == '\t' || c == '\r') continue;
if (c == '/') { state = 1; continue; }
if (c == '#') { state = 4; continue; }
return false;
case 1: // have seen a single slash after start of line
if (c == '/') { state = 2; continue; }
return false;
case 2: // have seen two or more slashes
if (c == '/') continue;
if (c == ' ' || c == '\t' || c == '\r') { state = 3; continue; }
if (c == '#') { state = 4; continue; }
return false;
case 3: // have seen a space after two or more slashes
if (c == ' ' || c == '\t' || c == '\r') continue;
if (c == '#') { state = 4; continue; }
return false;
case 4: // have seen a '#'
leadKeyValue[0] = line.substring(mark, i-1);
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { mark = i; state = 5; continue; }
return false;
case 5: // an ascii char followed the '#'
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) continue;
if (c == ' ' || c == '\t' || c == '\n') {
String key = line.substring(mark, i).toLowerCase();
if (key.equals("ifdef") ||
key.equals("ifndef") ||
key.equals("else") ||
key.equals("endif") ||
key.equals("undef") ||
key.equals("define") ||
key.equals("if") ||
key.equals("elif")) {
leadKeyValue[1] = key;
mark = i;
state = 6;
break loop;
}
}
return false;
default:
throw new IllegalStateException();
}
}
if (state == 6) {
leadKeyValue[2] = line.substring(mark, line.length()).trim();
return true;
}
return false; // never reached, does the compiler know this?
}
/**
* Perform same operation as matching on pat2. on exit
* keyRelValue contains the three strings key, rel, and value.
* 'key' is the portion before the relation (or final word). 'rel' is
* the relation, if present, either == or !=. 'value' is the final
* word. if there is a match, return true, else return false.
*/
static boolean pat2Match(String line, String[] keyRelVal) {
if (line.length() == 0) {
return false;
}
keyRelVal[0] = keyRelVal[1] = keyRelVal[2] = "";
int mark = 0;
int state = 0;
String command = null;
loop: for (int i = 0; i < line.length(); ++i) {
char c = line.charAt(i);
switch (state) {
case 0: // saw beginning or space, no rel yet
if (c == ' ' || c == '\t' || c == '\n') {
continue;
}
if ((c == '!' || c == '=')) {
return false;
}
state = 1;
continue;
case 1: // saw start of a word
if (c == ' ' || c == '\t') {
state = 2;
}
else if (c == '(') {
command = line.substring(0, i).trim();
if (!command.equals("defined")) {
return false;
}
keyRelVal[0] = command;
state = 2;
}
else if (c == '!' || c == '=') {
state = 3;
}
continue;
case 2: // saw end of word, and space
if (c == ' ' || c == '\t') {
continue;
}
else if (command == null && c == '(') {
continue;
}
else if (c == '!' || c == '=') {
state = 3;
continue;
}
keyRelVal[0] = line.substring(0, i-1).trim();
mark = i;
state = 4;
break loop;
case 3: // saw end of word, and '!' or '='
if (c == '=') {
keyRelVal[0] = line.substring(0, i-1).trim();
keyRelVal[1] = line.substring(i-1, i+1);
mark = i+1;
state = 4;
break loop;
}
return false;
default:
break;
}
}
switch (state) {
case 0:
return false; // found nothing
case 1:
case 2:
keyRelVal[0] = line.trim(); break; // found only a word
case 3:
return false; // found a word and '!' or '=" then end of line, incomplete
case 4:
keyRelVal[2] = line.substring(mark).trim(); // found a word, possible rel, and who knows what
if (command != null) {
int len = keyRelVal[2].length();
if (keyRelVal[2].charAt(len - 1) != ')') {
// closing parenthesis is missing
return false;
}
keyRelVal[2] = keyRelVal[2].substring(0, len - 1).trim();
}
break;
default:
throw new IllegalStateException();
}
return true;
}
static String pat3Match(String line) {
int state = 0;
loop: for (int i = 0; i < line.length(); ++i) {
char c = line.charAt(i);
switch(state) {
case 0: if (c == ' ' || c == '\t') continue;
if (c == '/') { state = 1; continue; }
break loop;
case 1:
if (c == '/') { state = 2; continue; }
break loop;
case 2:
if (c == '#') { state = 3; continue; }
break loop;
case 3:
if (c == '#') return line.substring(0, i+1);
break loop;
default:
break loop;
}
}
return null;
}
}