/*
* Copyright (C) 2006 SQL Explorer Development Team
* http://sourceforge.net/projects/eclipsesql
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package net.sourceforge.sqlexplorer.parsers.scp;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.ListIterator;
import net.sourceforge.sqlexplorer.parsers.ParserException;
import net.sourceforge.sqlexplorer.parsers.QueryParser;
import net.sourceforge.sqlexplorer.parsers.Tokenizer;
import net.sourceforge.sqlexplorer.parsers.Tokenizer.Token;
import net.sourceforge.sqlexplorer.parsers.Tokenizer.TokenType;
/**
* Implements extensions to the underlying database platform via structured comments. Each structured
* comment begins with ${ as the FIRST two characters in the comment - no space can occur between the
* start of the comment and the ${. From there up to the first } is a command, and the rest of the
* comment is data for the command to act on.
*
* The commands supported are:
*
* ${define macro-name} value
* Defines a macro called macro-name and assigns a value
*
* ${ifdef [!]macro-name} [data]
* If macro-name has been defined, then the data in the comment (IE the text outside the
* ${...}) is uncommented. If the optional ! is provided then the macro-name must NOT have
* been defined. If data has not been given, then ordinary code between ifdef and the next
* else or endif is commented depending on whether macro-name is defined
*
* ${else} [data]
* If the last structured comment was an ifdef which evaluated to false, then the data in
* the comment is uncommented if
*
* ${endif}
* Only needed to complete the multi-line versions of ifdef/else.
*
* ${undef macro-name}
* Undefines the given macro name
*
* ${ref macro-name}
* Causes the value of macro-name to be output instead of the comment; note that no error is
* raised if macro-name has not been defined.
*
* ${endref}
* Needed to complete multi-line versions of ref
*
* ${parameter name [("output"|"inout")] [datatype] [arguments]} [value]
* Declares a named parameter, it's datatype (decimal, int, string, or cursor), whether it is
* an output variable, and an optional value. If the datatype is "cursor" then it is an output
* variable only; if not specified, it is input only. Parameters are specified in queries
* using the notation ":name", eg "select * from people where age = :agetofind". If input
* or output is not specified, then input is assumed (unless it is a cursor). Some datatypes
* have additional parameters - eg parameters of type "date", where the format can be specified.
*
* Up and coming:
* ${question [id=id] [datatype=(char|int|decimal|date)]} data
* Interactively asks a user a question before running; the text of the question is in data,
* the default datatype is char. ID is used so that subsequent executions can remember the
* last value typed for that ID.
*
* ${date 'date-in-locale'}
* Replaced with parameter substitution to provide a date; date-in-locale is the date
* specified in the client's default locale.
*
* ${content-type column=column-name, type='mime-type'}
* Annotates a column named column-name as having the given mime-type, overriding the default
* assumption by SQLExplorer. Typically intended for marking BLOBs as (for example) images,
* can also be used to determine that varchars or CLOBs might contain XML, etc etc.
*
* Examples:
* NOTE: in all of these examples, -- comments are used; the Java/C style of slash-asterisk to
* asterisk-slash also works, but there's no examples here because Java will not understand it.
*
* --${define DEBUG} true
* --${ifdef DEBUG} dbms_output.put_line('in debug mode...');
* --${else} dbms_output.put_line('not in debug mode');
* --${endif}
*
* Defines a macro called DEBUG, and outputs one of the dbms_output statements. If loaded through
* a standard SQL tool, both statements would remain comments.
*
* --${ifdef DEBUG} dbms_output.put_line('in debug mode');
* dbms_output.put_line('The default is not in debug mode');
* --${endif}
*
* If DEBUG is defined, then the first dbms_output statement would be uncommented, but the
* second will become commented; however if code was loaded through a standard SQL tool then
* the second line will run by default.
*
* --${define DEFAULT_DATE} to_date('1980-JAN-01')
* ...snip...
* insert into mytable (id, when)
* values (
* 1,
* --${ref DEFAULT_DATE}
* );
*
* This defines a macro called DEFAULT_DATE and assigns it the value "to_date('1980-JAN-01')";
* later on it uses ref to get the value of DEFAULT_DATE to insert into the second column
*
* --${define SOME_SQL} select * from blah;
* ...snip...
* --${ref SOME_SQL}
* null;
* --${endref}
*
* This defines SOME_SQL as "select * from blah;" and refers to it later. However, because the
* code uses <code>endref</code>, then the non-comment between ref and endref is commented out;
* this allows a standard SQL tool to still be able to compile with a default.
*
* @author John Spackman
*/
public class StructuredCommentParser {
/*
* Type of command; knows how to instantiate a Command
*/
public enum CommandType {
DEFINE {
@Override
public Command createInstance(StructuredCommentParser parser, Token comment, Tokenizer tokenizer, CharSequence data) throws ParserException {
return new DefineCommand(parser, comment, tokenizer, data);
}
},
UNDEF {
@Override
public Command createInstance(StructuredCommentParser parser, Token comment, Tokenizer tokenizer, CharSequence data) throws ParserException {
return new UndefCommand(parser, comment, tokenizer, data);
}
},
IFDEF {
@Override
public Command createInstance(StructuredCommentParser parser, Token comment, Tokenizer tokenizer, CharSequence data) throws ParserException {
return new IfdefCommand(parser, comment, tokenizer, data);
}
},
ELSE {
@Override
public Command createInstance(StructuredCommentParser parser, Token comment, Tokenizer tokenizer, CharSequence data) throws ParserException {
return new ElseCommand(parser, comment, tokenizer, data);
}
},
ENDIF {
@Override
public Command createInstance(StructuredCommentParser parser, Token comment, Tokenizer tokenizer, CharSequence data) throws ParserException {
return new EndifCommand(parser, comment, tokenizer, data);
}
},
REF {
@Override
public Command createInstance(StructuredCommentParser parser, Token comment, Tokenizer tokenizer, CharSequence data) throws ParserException {
return new RefCommand(parser, comment, tokenizer, data);
}
},
ENDREF {
@Override
public Command createInstance(StructuredCommentParser parser, Token comment, Tokenizer tokenizer, CharSequence data) throws ParserException {
return new EndrefCommand(parser, comment, tokenizer, data);
}
},
PARAMETER {
@Override
public Command createInstance(StructuredCommentParser parser, Token comment, Tokenizer tokenizer, CharSequence data) throws ParserException {
new ParameterCommand(parser, comment, tokenizer, data);
return null;
}
};
public abstract Command createInstance(StructuredCommentParser parser, Token comment, Tokenizer tokenizer, CharSequence data) throws ParserException;
};
// The QueryParser
protected QueryParser parser;
// Master buffer
protected StringBuffer buffer;
// Structured comments
protected LinkedList<Command> commands = new LinkedList<Command>();
// Macros which have been defined
HashMap<String, CharSequence> macros = new HashMap<String, CharSequence>();
/**
* Constructor. <code>buffer</code> must be a writable buffer against which all
* tokens past to addComment have been parsed.
* @param buffer
*/
public StructuredCommentParser(QueryParser parser, StringBuffer buffer) {
super();
this.parser = parser;
this.buffer = buffer;
}
/**
* Adds a comment to the list; it will be ignored if it is not a structured comment
* @param token
* @throws StructuredCommentException - usually if the comment begins ${ but is unparsable
*/
public void addComment(Token comment) throws ParserException {
Command command = createCommand(comment);
if (command == null)
return;
/*
* Conditional constructs (ie IFDEF, ELSE, and ENDIF) are linked together
* so that we can easily identify which bits of code go through to the server
*/
// If we get an ELSE
if (command instanceof ElseCommand) {
ElseCommand elseCmd = (ElseCommand)command;
Command last = null;
int nestingDepth = 0;
// Starting at the most recent structured comment, work towards the first
// looking for the IFDEF that we're part of
ListIterator<Command> iter = commands.listIterator(commands.size());
while (iter.hasPrevious()) {
last = iter.previous();
// If we see an ENDIF on the way, remember the nesting so that we skip
// it's IFDEF
if (last instanceof EndifCommand)
nestingDepth++;
// Found an IFDEF?
else if (last instanceof IfdefCommand) {
if (nestingDepth == 0)
break;
else
nestingDepth--;
}
}
if (last == null)
throw new StructuredCommentException("Unexpected 'else' command - no previous ifdef", comment);
IfdefCommand ifdef = (IfdefCommand)last;
if (ifdef.next != null)
throw new StructuredCommentException("Unexpected 'else' command - else already encountered for this ifdef", comment);
// Link it to us
ifdef.next = elseCmd;
elseCmd.previous = ifdef;
// ENDIF construct
} else if (command instanceof EndifCommand) {
EndifCommand endif = (EndifCommand)command;
Command last = null;
int nestingDepth = 0;
// Starting at the most recent structured comment, work towards the first
// looking for the IFDEF that we're part of
ListIterator<Command> iter = commands.listIterator(commands.size());
while (iter.hasPrevious()) {
last = iter.previous();
// If we see an ENDIF on the way, remember the nesting so that we skip
// it's IFDEF
if (last instanceof EndifCommand)
nestingDepth++;
// IFDEF? Check nesting, and only accept it if we're not on a nested IFDEF
else if (last instanceof IfdefCommand) {
if (nestingDepth == 0)
break;
else
nestingDepth--;
// ELSE command - also affected by nesting
} else if (last instanceof ElseCommand) {
if (nestingDepth == 0)
break;
}
}
if (last == null)
throw new StructuredCommentException("Unexpected 'endif' command - no previous ifdef", comment);
PeeredCommand cond = (PeeredCommand)last;
if (cond.next != null)
throw new StructuredCommentException("Unexpected 'endif' command - endif already encountered for this ifdef/else", comment);
// Store it
cond.next = endif;
endif.previous = cond;
// EndRef
} else if (command instanceof EndrefCommand) {
EndrefCommand endref = (EndrefCommand)command;
Command last = commands.size() == 0 ? null : (Command)commands.getLast();
if (last == null || !(last instanceof RefCommand))
throw new StructuredCommentException("Unexpected endref - no preceeding ref", comment);
// Store it
RefCommand ref = (RefCommand)last;
ref.endref = endref;
endref.ref = ref;
}
// Add the new command
commands.add(command);
}
/**
* Applies structured comments onto onto the buffer.
*/
public void process() {
ListIterator<Command> iter = commands.listIterator();
while (iter.hasNext()) {
Command command = iter.next();
if (command.commandType == CommandType.DEFINE) {
DefineCommand def = (DefineCommand)command;
macros.put(def.macroName, def.data == null ? "" : def.data);
delete(iter, def, def, true);
} else if (command.commandType == CommandType.UNDEF) {
UndefCommand def = (UndefCommand)command;
macros.remove(def.macroName);
delete(iter, def, def, true);
} else if (command.commandType == CommandType.IFDEF) {
IfdefCommand def = (IfdefCommand)command;
// A next means its a multi-line ifdef
if (def.next != null) {
// If it evaluates to false, then we have to delete the content
if (!def.evaluate()) {
delete(iter, def, def.next, false);
// Else delete the comment
} else
delete(iter, def, def, true);
// Single-line ifdef
} else {
// If it evaluates to true we have to replace the comment with the data
// otherwise we just leave it commented out
if (def.evaluate())
replace(iter, def, def.data);
// Else delete the comment
else
delete(iter, def, def, true);
}
} else if (command.commandType == CommandType.ELSE) {
ElseCommand elseCmd = (ElseCommand)command;
IfdefCommand ifdef = (IfdefCommand)elseCmd.previous;
// A next means that it's a multi-line "else"
if (elseCmd.next != null) {
// If the ifdef was true, then delete the content
if (ifdef.evaluate())
delete(iter, elseCmd, elseCmd.next, true);
// Else delete the comment
else
delete(iter, elseCmd, elseCmd, true);
// Single line "else"
} else {
// If the IFDEF failed, replace the ELSE comment with the ELSE data
if (!ifdef.evaluate())
replace(iter, elseCmd, elseCmd.data);
// Else delete the comment
else
delete(iter, elseCmd, elseCmd, true);
}
} else if (command.commandType == CommandType.REF) {
RefCommand ref = (RefCommand)command;
CharSequence seq = macros.get(ref.macroName);
// An ENDREF means it's multi-line
if (ref.endref != null) {
// If the macro does not exist, use the content as a default
if (seq == null) {
delete(iter, ref, ref, true);
iter.next();
delete(iter, ref.endref, ref.endref, true);
} else
replace(iter, ref, ref.endref, seq);
} else {
if (seq == null)
seq = "";
replace(iter, ref, seq);
}
} else if (command.commandType == CommandType.ENDIF) {
delete(iter, command, command, true);
} else if (command.commandType == CommandType.PARAMETER) {
}
}
}
/**
* Attempts to create a AbstractCommand from a comment token
* @param comment the comment to parse
* @return the new AbstractCommand, or null if it is not a structured comment
* @throws StructuredCommentException
*/
protected Command createCommand(Token comment) throws ParserException {
StringBuffer sb = new StringBuffer(comment);
sb.delete(0, 2);
if (comment.getTokenType() == TokenType.ML_COMMENT)
sb.delete(sb.length() - 2, sb.length());
// Make sure it begins ${, but silently ignore it if not
int pos = sb.indexOf("}", 2);
if (sb.length() < 3 || !sb.substring(0, 2).equals("${") || pos < 0)
return null;
// Extract the command (ie the bit between "${" and "}") and the data (the bit after the "}")
String data = null;
if (pos < sb.length()) {
data = sb.substring(pos + 1).trim();
if (data.length() == 0)
data = null;
}
sb = new StringBuffer(sb.substring(2, pos));
// ...and has a word as the first token
Tokenizer tokenizer = new Tokenizer(sb);
Token token = tokenizer.nextToken();
if (token == null)
return null;
if (token.getTokenType() != TokenType.WORD)
throw new StructuredCommentException("Unexpected command in structured comment: " + token.toString(), comment);
// Create a new AbstractCommand
CommandType type;
try {
// I've kept the determination of CommandType outside of the constructor in case we want
// to instantiate different classes for the different commands.
type = CommandType.valueOf(token.toString().toUpperCase());
} catch(IllegalArgumentException e) {
throw new StructuredCommentException("Unrecognised structured comment command \"" + token.toString() + "\"", comment);
}
return type.createInstance(this, comment, tokenizer, data);
}
/**
* Deletes a section from the buffer and adjusts the offsets in the upcoming tokens accordingly; the section
* to be deleted starts at the first character of startCmd and continues up to either the first character of
* endCmd if endInclusive is false, or the last character of endCmd is endInclusive is true
* @param iter iterator which will return the future tokens
* @param startCmd
* @param endCmd
* @param endInclusive
*/
protected void delete(ListIterator<Command> iter, Command startCmd, Command endCmd, boolean endInclusive) {
int numLines = endCmd.comment.getLineNo() - startCmd.comment.getLineNo();
for (char c : endCmd.comment)
if (c == '\n')
numLines++;
if (numLines != 0)
parser.addLineNoOffset(startCmd.comment.getLineNo(), -numLines);
int start = startCmd.comment.getStart();
int end = endInclusive ? endCmd.comment.getEnd() : endCmd.comment.getStart();
buffer.delete(start, end);
int offset = -(end - start);
if (startCmd != endCmd) {
while (iter.next() != endCmd)
;
if (!endInclusive)
iter.previous();
}
iter = commands.listIterator(iter.nextIndex());
while (iter.hasNext()) {
Command command = iter.next();
command.comment.applyOffset(offset);
}
}
/**
* Replaces a section of the buffer, starting with the first character of startCmd and ending with the last
* character of endCmd; after replacement it adjusts the offsets in the upcoming tokens
* @param iter iterator which will return the future tokens
* @param startCmd
* @param endCmd
* @param replacement the replacement text
*/
protected void replace(ListIterator<Command> iter, Command startCmd, Command endCmd, CharSequence replacement) {
int numLines = endCmd.comment.getLineNo() - startCmd.comment.getLineNo();
for (char c : endCmd.comment)
if (c == '\n')
numLines++;
for (int i = 0; i < replacement.length(); i++)
if (replacement.charAt(i) == '\n')
numLines--;
parser.addLineNoOffset(startCmd.comment.getLineNo(), -numLines);
int start = startCmd.comment.getStart();
int end = endCmd.comment.getEnd();
buffer.delete(start, end);
buffer.insert(start, replacement);
int offset = -(end - start) + replacement.length();
if (startCmd != endCmd) {
while (iter.next() != endCmd)
;
}
iter = commands.listIterator(iter.nextIndex());
while (iter.hasNext()) {
Command command = iter.next();
command.comment.applyOffset(offset);
}
}
/**
* Replaces a section of the buffer, starting with the first character of startCmd and ending with the last
* character of startCmd; after replacement it adjusts the offsets in the upcoming tokens
* @param iter iterator which will return the future tokens
* @param startCmd
* @param replacement the replacement text
*/
protected void replace(ListIterator<Command> iter, Command token, CharSequence replacement) {
replace(iter, token, token, replacement);
}
}