org.hsqldb.util.preprocessor.Preprocessor Maven / Gradle / Ivy
Show all versions of hsqldb Show documentation
/* Copyright (c) 2001-2022, 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 6550 2022-06-08 11:09:08Z 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 lexicographic elements are :
*
*
* ASSIGN : '='
* EQ : '=='
* LT : {@code '<'}
* LTE : {@code '<='}
* GT : {@code '>'}
* GTE : {@code '>='}
* OR : ('|' | '||')
* XOR : '^'
* AND : {@code ('&' | '&&')}
* 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 lexicographic 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 {@link Option 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 2.7.0
* @since 1.8.1
*/
public class Preprocessor {
// =========================================================================
// ------------------------------- Public API ------------------------------
// =========================================================================
/**
* Preprocesses the specified list of files.
*
*
* @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 {
// log("sourceDir: " + sourceDir);
// log("targetDir: " + targetDir);
// log("fileNames: " + Arrays.asList(fileNames));
// log("altExt : " + altExt);
// log("encoding : " + encoding);
// log("options : " + Option.toOptionsString(options));
// log("defines : " + defines);
// log("resolver : " + resolver);
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.
*
*
* @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("sourceDir : " + sourceDir);
// log("targetDir : " + targetDir);
// log("fileName : " + fileName);
// log("altExt : " + altExt);
// log("encoding : " + encoding);
// log("options : " + Option.toOptionsString(options));
// log("defines : " + defines);
// log("resolver : " + resolver);
log("sourcePath: " + sourcePath);
log("targetPath: " + targetPath);
}
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()
@SuppressWarnings("UnnecessaryBoxing")
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 final String documentPath;
private final String encoding;
private final int options;
private final IResolver resolver;
private final Document document;
private final Defines defines;
private final 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.contentEquals(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;
}
}
}
@SuppressWarnings("unchecked")
private void statePush() {
this.stack.push(STATES[this.state]);
}
@SuppressWarnings("UnnecessaryUnboxing")
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");
}
if (Option.isVerbose(options)) {
log("resolve path: " + path);
}
if (path.contains("${")) {
path = this.resolver.resolveProperties(path);
if (Option.isVerbose(options)) {
log("resolved properties: " + path);
}
}
File file = this.toCanonicalOrAbsoluteFile(path);
if (Option.isVerbose(options)) {
log("resolved file: " + file.getAbsolutePath());
}
try {
path = file.getCanonicalPath();
} catch (IOException ex) {
path = file.getAbsolutePath();
}
if (Option.isVerbose(options)) {
log("resolved path: " + path);
}
return path;
}
// ------------------------------ Conversion -------------------------------
private String toSourceLine(final Line line) {
final String indent = line.getIndent();
final String text = line.getText();
return (isHidingLines())
? Option.isIndent(this.options)
? indent + Line.HIDE_DIRECTIVE + text
: Line.HIDE_DIRECTIVE + indent + text
: indent + text;
}
private File toCanonicalOrAbsoluteFile(String path) {
File file = new File(path);
if (!file.isAbsolute()) {
File parent = (new File(this.documentPath)).getParentFile();
file = new File(parent, path);
}
try {
return file.getCanonicalFile();
} catch (IOException 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);
}
}