/* Copyright (c) 2001-2007, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


package org.hsqldb.util.preprocessor;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Stack;

/* $Id: Preprocessor.java 5793 2018-01-06 13:12:38Z fredt $ */

Simple text document preprocessor.

Aims specifically at transforming the HSQLDB codebase to one of a small number of specific build targets, while keeping complexity and external dependencies to a minimum, yet providing an environment that is sufficiently powerful to solve most easily imaginable preprocessing scenarios. Supports the following (case-sensitive) directives:

  • //#def[ine] IDENT (ASSIGN? (STRING | NUMBER | IDENT) )?
  • //#elif BOOLEXPR
  • //#elifdef IDENT
  • //#elifndef IDENT
  • //#else
  • //#endif
  • //#endinclude
  • //#if BOOLEXPR
  • //#ifdef IDENT
  • //#ifndef IDENT
  • //#include FILEPATH
  • //#undef[ine] IDENT
where BOOLEXPR is:
( IDENT
| IDENT ( EQ |  LT | LTE | GT | GTE ) VALUE
| BOOLEXPR { OR | XOR | AND } BOOLEXPR
| NOT BOOLEXPR
| LPAREN BOOLEXPR RPAREN )
and VALUE is :
( STRING
| NUMBER
| IDENT )
and lexographic elements are :
ASSIGN     : '='
EQ         : '=='
LT         : '<'
LTE        : '<='
GT         : '>'
GTE        : '>='
OR         : ('|' | '||')
XOR        : '^'
AND        : ('&' | '&&')
NOT        : '!'
DQUOTE     : '"'
LPAREN     : '('
RPAREN     : ')'
DOT        : '.'
DIGIT      : ['0'..'9']
EOL        : ('\n' | '\r' | '\n\r')
SPACE      : (' ' | '\t')
NON_DQUOTE : { ANY_UNICODE_CHARACTER_EXCEPT_DQUOTE_OR_EOL } -- see the unicode spec
NON_SPACE  : { ANY_UNICODE_CHARACTER_EXCEPT_SPACE_OR_EOL }  -- see the unicode spec
WS         : { JAVA_WS } -- see java.lang.Character
NON_WS     : { ANY_UNICODE_CHARACTER_EXCEPT_WS_OR_EOL }
STRING     : DQUOTE NON_DQUOTE* DQUOTE
NUMBER     : DIGIT+ (DOT DIGIT*)?
IDENT      : JAVA_IDENT_START JAVA_IDENT_PART*              -- see java.lang.Character
FILEPATH   : NON_SPACE (ANY_UNICODE_CHARACTER* NON_WS)?     -- i.e. trailing SPACE elements are ignored
The lexographic definitions above use the BNF conventions :
'?' -> zero or one
'*' -> zero or more
'+' -> one or more
Directives may be arbitrarily indented; there is an option (INDENT) to set or unset directive indentation on output. There is also an option (FILTER) to remove directive lines from output. See Option for other preprocessor options.

'//#ifxxx' directives may be nested to arbitrary depth, may be chained with an arbitrary number of '//#elifxxx' directives, may be optionally followed by a single '//#else' directive, and must be terminated by a single '//#endif' directive.

Each '//#include' directive must be terminated by an '//#endinclude' directive; lines between '//#include' and '//#endinclude' are replaced by the content retrieved from the specified FILEPATH.

Included files are preprocessed in a nested scope that inherits the defined symbols of the including scope. Directive lines in included files are always excluded from output.

Design Notes

There are many better/more sophisticated preprocessors/templating engines out there. FreeMaker and Velocity come to mind immediately. Another--the NetBeans MIDP preprocessor--was the direct inspiration for this class.

Other options were rejected because the work of creating this class appeared to be less than dealing with the complexity and dependency issues of hooking up to external libraries. The NetBeans preprocessor, in particular, was rejected because it was not immediately evident how to invoke it independently from the IDE, how to make it available to non-MIDP projects from within the IDE or how to isolate the correct OpenIDE jars to allow stand-alone operation.

Author:Campbell Burnet (campbell-burnet@users dot sourceforge.net)
Version:1.8.1
Since:1.8.1
/** * Simple text document preprocessor. <p> * * Aims specifically at transforming the HSQLDB codebase to one of a small * number of specific build targets, while keeping complexity and external * dependencies to a minimum, yet providing an environment that is * sufficiently powerful to solve most easily imaginable preprocessing * scenarios. * * Supports the following (case-sensitive) directives: <p> * * <ul> * <li>//#def[ine] IDENT (ASSIGN? (STRING | NUMBER | IDENT) )? * <li>//#elif BOOLEXPR * <li>//#elifdef IDENT * <li>//#elifndef IDENT * <li>//#else * <li>//#endif * <li>//#endinclude * <li>//#if BOOLEXPR * <li>//#ifdef IDENT * <li>//#ifndef IDENT * <li>//#include FILEPATH * <li>//#undef[ine] IDENT * </ul> * * where BOOLEXPR is: * * <pre> * ( IDENT * | IDENT ( EQ | LT | LTE | GT | GTE ) VALUE * | BOOLEXPR { OR | XOR | AND } BOOLEXPR * | NOT BOOLEXPR * | LPAREN BOOLEXPR RPAREN ) *</pre> * * and VALUE is : * * <pre> * ( STRING * | NUMBER * | IDENT ) * </pre> * * and lexographic elements are : * * <pre> * ASSIGN : '=' * EQ : '==' * LT : '<' * LTE : '<=' * GT : '>' * GTE : '>=' * OR : ('|' | '||') * XOR : '^' * AND : ('&' | '&&') * NOT : '!' * DQUOTE : '"' * LPAREN : '(' * RPAREN : ')' * DOT : '.' * DIGIT : ['0'..'9'] * EOL : ('\n' | '\r' | '\n\r') * SPACE : (' ' | '\t') * NON_DQUOTE : { ANY_UNICODE_CHARACTER_EXCEPT_DQUOTE_OR_EOL } -- see the unicode spec * NON_SPACE : { ANY_UNICODE_CHARACTER_EXCEPT_SPACE_OR_EOL } -- see the unicode spec * WS : { JAVA_WS } -- see java.lang.Character * NON_WS : { ANY_UNICODE_CHARACTER_EXCEPT_WS_OR_EOL } * STRING : DQUOTE NON_DQUOTE* DQUOTE * NUMBER : DIGIT+ (DOT DIGIT*)? * IDENT : JAVA_IDENT_START JAVA_IDENT_PART* -- see java.lang.Character * FILEPATH : NON_SPACE (ANY_UNICODE_CHARACTER* NON_WS)? -- i.e. trailing SPACE elements are ignored * </pre> * * The lexographic definitions above use the BNF conventions : * * <pre> * '?' -> zero or one * '*' -> zero or more * '+' -> one or more * </pre> * * Directives may be arbitrarily indented; there is an option (INDENT) to set * or unset directive indentation on output. There is also an option (FILTER) * to remove directive lines from output. See {@link Option Option} for other * preprocessor options. <p> * * '//#ifxxx' directives may be nested to arbitrary depth, * may be chained with an arbitrary number of '//#elifxxx' directives, * may be optionally followed by a single '//#else' directive, and * must be terminated by a single '//#endif' directive. <p> * * Each '//#include' directive must be terminated by an '//#endinclude' * directive; lines between '//#include' and '//#endinclude' are replaced * by the content retrieved from the specified FILEPATH. <p> * * Included files are preprocessed in a nested scope that inherits the * defined symbols of the including scope. Directive lines in included files * are always excluded from output. <p> * * <b>Design Notes</b><p> * * There are many better/more sophisticated preprocessors/templating * engines out there. FreeMaker and Velocity come to mind immediately. * Another--the NetBeans MIDP preprocessor--was the direct inspiration for * this class. <p> * * Other options were rejected because the work of creating this class appeared * to be less than dealing with the complexity and dependency issues of hooking * up to external libraries. * * The NetBeans preprocessor, in particular, was rejected because it was * not immediately evident how to invoke it independently from the IDE, * how to make it available to non-MIDP projects from within the IDE or how to * isolate the correct OpenIDE jars to allow stand-alone operation. <p> * * @author Campbell Burnet (campbell-burnet@users dot sourceforge.net) * @version 1.8.1 * @since 1.8.1 */
public class Preprocessor { // ========================================================================= // ------------------------------- Public API ------------------------------ // =========================================================================
Preprocesses the specified list of files.

Params:
  • sourceDir – under which input files are located
  • targetDir – under which output files are to be written
  • fileNames – to be preprocessed
  • altExt – to use for output file names
  • encoding – with which to write output files
  • options – used to control preprocessing
  • defines – CSV list of symbol definition expressions
  • resolver – with which to perform property and path expansions
Throws:
  • PreprocessorException – if an error occurs while loading, preprocessing or saving the result of preprocessing one of the specified input files
/** * Preprocesses the specified list of files. <p> * * @param sourceDir under which input files are located * @param targetDir under which output files are to be written * @param fileNames to be preprocessed * @param altExt to use for output file names * @param encoding with which to write output files * @param options used to control preprocessing * @param defines CSV list of symbol definition expressions * @param resolver with which to perform property and path expansions * @throws PreprocessorException if an error occurs while loading, * preprocessing or saving the result of preprocessing one of the * specified input files */
public static void preprocessBatch(File sourceDir, File targetDir, String[] fileNames, String altExt, String encoding, int options, String defines, IResolver resolver) throws PreprocessorException { for (int i = 0; i < fileNames.length; i++) { String fileName = fileNames[i]; try { preprocessFile(sourceDir, targetDir, fileName, altExt, encoding, options, defines, resolver); } catch (PreprocessorException ppe) { if (!Option.isVerbose(options)) { log(fileName + " ... not modified, " + ppe.getMessage()); } throw ppe; } } }
Preprocesses a single file.

Params:
  • sourceDir – under which the input file is located
  • targetDir – under which the output file is to be written
  • fileName – to be preprocessed
  • altExt – to use for output file name
  • encoding – with which to write output file
  • options – used to control preprocessing
  • defines – CSV list of symbol definition expressions
  • resolver – with which to perform property and path expansions
Throws:
  • PreprocessorException – if an error occurs while loading, preprocessing or saving the result of preprocessing the specified input file
/** * Preprocesses a single file. <p> * * @param sourceDir under which the input file is located * @param targetDir under which the output file is to be written * @param fileName to be preprocessed * @param altExt to use for output file name * @param encoding with which to write output file * @param options used to control preprocessing * @param defines CSV list of symbol definition expressions * @param resolver with which to perform property and path expansions * @throws PreprocessorException if an error occurs while loading, * preprocessing or saving the result of preprocessing the * specified input file */
public static void preprocessFile(File sourceDir, File targetDir, String fileName, String altExt, String encoding, int options, String defines, IResolver resolver) throws PreprocessorException { String sourcePath = translatePath(sourceDir, fileName, null); String targetPath = translatePath(targetDir, fileName, altExt); File targetFile = new File(targetPath); File backupFile = new File(targetPath + "~"); boolean sameDir = sourceDir.equals(targetDir); boolean sameExt = (altExt == null); boolean verbose = Option.isVerbose(options); boolean testOnly = Option.isTestOnly(options); boolean backup = Option.isBackup(options); Preprocessor preprocessor = new Preprocessor(sourcePath, encoding, options, resolver, defines); if (verbose) { log("Reading \"" + sourcePath + "\""); } preprocessor.loadDocument(); boolean modified = preprocessor.preprocess(); boolean rewrite = modified || !sameDir || !sameExt; if (!rewrite) { if (verbose) { log(fileName + " ... not modified"); } return; } else if (verbose) { log(fileName + " ... modified"); } if (testOnly) { return; } try { targetFile.getParentFile().mkdirs(); } catch (Exception e) { throw new PreprocessorException("mkdirs failed \"" + targetFile + "\": " + e); // NOI18N } backupFile.delete(); if (targetFile.exists() && !targetFile.renameTo(backupFile)) { throw new PreprocessorException("Rename failed: \"" + targetFile + "\" => \"" + backupFile +"\"" ); // NOI18N } if (verbose) { log("Writing \"" + targetPath + "\""); } preprocessor.saveDocument(targetPath); if (!backup) { backupFile.delete(); } } // ========================================================================= // ----------------------------- Implementation ---------------------------- // ========================================================================= // Fields // static static final int CONDITION_NONE = 0; static final int CONDITION_ARMED = 1; static final int CONDITION_IN_TRUE = 2; static final int CONDITION_TRIGGERED = 3; // optimization - zero new object burn rate for statePush() static final Integer[] STATES = new Integer[] { new Integer(CONDITION_NONE), new Integer(CONDITION_ARMED), new Integer(CONDITION_IN_TRUE), new Integer(CONDITION_TRIGGERED) }; // instance private String documentPath; private String encoding; private int options; private IResolver resolver; private Document document; private Defines defines; private Stack stack; private int state; // Constructors private Preprocessor(String documentPath, String encoding, int options, IResolver resolver, String predefined) throws PreprocessorException { if (resolver == null) { File parentDir = new File(documentPath).getParentFile(); this.resolver = new BasicResolver(parentDir); } else { this.resolver = resolver; } if (predefined == null || predefined.trim().length() == 0) { this.defines = new Defines(); } else { predefined = this.resolver.resolveProperties(predefined); this.defines = new Defines(predefined); } this.documentPath = documentPath; this.encoding = encoding; this.options = options; this.document = new Document(); this.stack = new Stack(); this.state = CONDITION_NONE; } private Preprocessor(Preprocessor other, Document include) { this.document = include; this.encoding = other.encoding; this.stack = new Stack(); this.state = CONDITION_NONE; this.options = other.options; this.documentPath = other.documentPath; this.resolver = other.resolver; this.defines = other.defines; } // Main entry point private boolean preprocess() throws PreprocessorException { this.stack.clear(); this.state = CONDITION_NONE; // optimization - eliminates a full document copy and a full document // equality test for files with no preprocessor // directives if (!this.document.contains(Line.DIRECTIVE_PREFIX)) { return false; } Document originalDocument = new Document(this.document); preprocessImpl(); if (this.state != CONDITION_NONE) { throw new PreprocessorException("Missing final #endif"); // NOI18N } if (Option.isFilter(options)) { // Cleanup all directives. for (int i = this.document.size() - 1; i >= 0; i--) { Line line = resolveLine(this.document.getSourceLine(i)); if (!line.isType(LineType.VISIBLE)) { this.document.deleteSourceLine(i); } } } return (!this.document.equals(originalDocument)); } private void preprocessImpl() throws PreprocessorException { int includeCount = 0; int lineCount = 0; while (lineCount < this.document.size()) { try { Line line = resolveLine(this.document.getSourceLine(lineCount)); switch(line.getType()) { case LineType.INCLUDE : { lineCount = processInclude(lineCount, line); break; } case LineType.VISIBLE : case LineType.HIDDEN : { this.document.setSourceLine(lineCount, toSourceLine(line)); if (Option.isVerbose(options)) { log((isHidingLines() ? "Commented: " : "Uncommented: ") + line); } lineCount++; break; } default : { processDirective(line); lineCount++; } } } catch (PreprocessorException ex) { throw new PreprocessorException(ex.getMessage() + " at line " + (lineCount + 1) + " in \"" + this.documentPath + "\""); // NOI18N } } } // -------------------------- Line-level Handlers -------------------------- private void processIf(boolean condition) { statePush(); this.state = isHidingLines() ? CONDITION_TRIGGERED : (condition) ? CONDITION_IN_TRUE : CONDITION_ARMED; } private void processElseIf(boolean condition) throws PreprocessorException { switch(state) { case CONDITION_NONE : { throw new PreprocessorException("Unexpected #elif"); // NOI18N } case CONDITION_ARMED : { if (condition) { this.state = CONDITION_IN_TRUE; } break; } case CONDITION_IN_TRUE : { this.state = CONDITION_TRIGGERED; break; } } } private void processElse() throws PreprocessorException { switch(state) { case CONDITION_NONE : { throw new PreprocessorException("Unexpected #else"); // NOI18N } case CONDITION_ARMED : { this.state = CONDITION_IN_TRUE; break; } case CONDITION_IN_TRUE : { this.state = CONDITION_TRIGGERED; break; } } } private void processEndIf() throws PreprocessorException { if (state == CONDITION_NONE) { throw new PreprocessorException("Unexpected #endif"); // NOI18N } else { statePop(); } } private void processDirective(Line line) throws PreprocessorException { switch(line.getType()) { case LineType.DEFINE : { if (!isHidingLines()) { this.defines.defineSingle(line.getArguments()); } break; } case LineType.UNDEFINE : { if (!isHidingLines()) { this.defines.undefine(line.getArguments()); } break; } case LineType.IF : { processIf(this.defines.evaluate(line.getArguments())); break; } case LineType.IFDEF : { processIf(this.defines.isDefined(line.getArguments())); break; } case LineType.IFNDEF : { processIf(!this.defines.isDefined(line.getArguments())); break; } case LineType.ELIF : { processElseIf(this.defines.evaluate(line.getArguments())); break; } case LineType.ELIFDEF : { processElseIf(this.defines.isDefined(line.getArguments())); break; } case LineType.ELIFNDEF : { processElseIf(!this.defines.isDefined(line.getArguments())); break; } case LineType.ELSE : { processElse(); break; } case LineType.ENDIF : { processEndIf(); break; } default : { throw new PreprocessorException("Unhandled line type: " + line); // NOI18N } } } private int processInclude(int lineCount, Line line) throws PreprocessorException { String path = resolvePath(line.getArguments()); boolean hidden = isHidingLines(); lineCount++; while (lineCount < this.document.size()) { line = resolveLine(this.document.getSourceLine(lineCount)); if (line.isType(LineType.ENDINCLUDE)) { break; } this.document.deleteSourceLine(lineCount); } if (!line.isType(LineType.ENDINCLUDE)) { throw new PreprocessorException("Missing #endinclude"); // NOI18N } if (!hidden) { Document include = loadInclude(path); Preprocessor preprocessor = new Preprocessor(this, include); preprocessor.preprocess(); int count = include.size(); for (int i = 0; i < count; i++) { String sourceLine = include.getSourceLine(i); if (resolveLine(sourceLine).isType(LineType.VISIBLE)) { this.document.insertSourceLine(lineCount++, sourceLine); } } } lineCount++; return lineCount; } // -------------------------- Preprocessor State --------------------------- private boolean isHidingLines() { switch(state) { case CONDITION_ARMED : case CONDITION_TRIGGERED: { return true; } default : { return false; } } } private void statePush() { this.stack.push(STATES[this.state]); } private void statePop() { this.state = ((Integer) stack.pop()).intValue(); } // ------------------------------ Resolution ------------------------------- private Line resolveLine(String line) throws PreprocessorException { return new Line(this.resolver.resolveProperties(line)); } private String resolvePath(String path) { if (path == null) { throw new IllegalArgumentException("path: null"); } String value = this.resolver.resolveProperties(path); File file = this.resolver.resolveFile(value); try { return file.getCanonicalPath(); } catch (IOException ex) { return file.getAbsolutePath(); } } // ------------------------------ Conversion ------------------------------- private String toSourceLine(Line line) { return (isHidingLines()) ? Option.isIndent(this.options) ? line.indent + Line.HIDE_DIRECTIVE + line.text : Line.HIDE_DIRECTIVE + line.indent + line.text : line.indent + line.text; } private File toCanonicalOrAbsoluteFile(String path) { File file = new File(path); if (!file.isAbsolute()) { path = (new File(this.documentPath)).getParent() + File.separatorChar + path; file = new File(path); } try { return file.getCanonicalFile(); } catch (Exception e) { return file.getAbsoluteFile(); } } // ------------------------------ Translation ------------------------------ private static String translatePath(File dir, String fileName, String ext) { return new StringBuffer(dir.getPath()).append(File.separatorChar). append(translateFileExtension(fileName,ext)).toString(); } private static String translateFileExtension(String fileName, String ext) { if (ext != null) { int pos = fileName.lastIndexOf('.'); fileName = (pos < 0) ? fileName + ext : fileName.substring(0, pos) + ext; } return fileName; } // ---------------------------------- I/O ---------------------------------- private Document loadInclude(String path) throws PreprocessorException { Document include = new Document(); File file = toCanonicalOrAbsoluteFile(path); try { return include.load(file, this.encoding); } catch (UnsupportedEncodingException uee) { throw new PreprocessorException("Unsupported encoding \"" + this.encoding + "\" loading include \"" + file + "\""); // NOI18N } catch (IOException ioe) { throw new PreprocessorException("Unable to load include \"" + file + "\": " + ioe); // NOI18N } } private void loadDocument() throws PreprocessorException { try { this.document.load(this.documentPath, this.encoding); } catch (UnsupportedEncodingException uee) { throw new PreprocessorException("Unsupported encoding \"" + this.encoding + "\" reading file \"" + this.documentPath + "\""); // NOI18N } catch (IOException ioe) { throw new PreprocessorException("Unable to read file \"" + this.documentPath + "\": " + ioe); // NOI18N } } private void saveDocument(Object target) throws PreprocessorException { try { if (this.document.size() > 0) { this.document.save(target, this.encoding); } } catch (UnsupportedEncodingException uee) { throw new PreprocessorException("Unsupported encoding \"" + this.encoding + "\" writing \"" + target + "\""); // NOI18N } catch (IOException ioe) { throw new PreprocessorException("Unable to write to \"" + target + "\": " + ioe); // NOI18N } } private static void log(Object toLog) { System.out.println(toLog); } }