/* 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 610 2008-12-22 15:54:18Z unsaved $ */ /** * 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 boucherb@users * @version 1.8.1 * @since 1.8.1 */ public class Preprocessor { // ========================================================================= // ------------------------------- Public API ------------------------------ // ========================================================================= /** * 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. <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); } }