/**
*
*/
package org.openntf.domino.helpers;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.Serializable;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.Stack;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.openntf.domino.Document;
import org.openntf.domino.Session;
import org.openntf.domino.utils.DominoUtils;
import org.openntf.domino.utils.Factory;
import org.openntf.domino.utils.Factory.SessionType;
import org.openntf.domino.utils.TypeUtils;
/**
* @author nfreeman
*
*/
public class Formula implements org.openntf.domino.ext.Formula, Serializable {
private static final Logger log_ = Logger.getLogger(Formula.class.getName());
private static final long serialVersionUID = 1L;
public static interface Decompiler {
public String decompile(byte[] compiled) throws Exception;
public String decompileB64(String compiled);
}
private Decompiler decompiler_;
public void setDecompiler(final Decompiler decomp) {
decompiler_ = decomp;
}
static class NoFormulaSetException extends RuntimeException {
/**
*
*/
private static final long serialVersionUID = 1L;
NoFormulaSetException() {
super("No expression has been set. There is nothing to evaluate.");
}
}
public static class FormulaUnableToDecompile extends RuntimeException {
/**
*
*/
private static final long serialVersionUID = 1L;
FormulaUnableToDecompile(final String original) {
super("Unable to decompile a compiled expression: " + original);
}
}
public static class FormulaSyntaxException extends RuntimeException {
/**
*
*/
private static final long serialVersionUID = 1L;
private Vector<?> syntaxDetails_;
// "errorMessage" : "errorLine" : "errorColumn" : "errorOffset" : "errorLength" : "errorText"
private String expression_;
FormulaSyntaxException(final String expression, final Vector<Object> syntaxDetails) {
super();
expression_ = expression;
syntaxDetails_ = syntaxDetails;
}
/*
* (non-Javadoc)
*
* @see java.lang.Throwable#getMessage()
*/
@Override
public String getMessage() {
if (syntaxDetails_ != null) {
return String.valueOf(syntaxDetails_.get(0));
} else {
return "Details unavailable";
}
}
/**
* @return the expression
*/
public String getExpression() {
return expression_;
}
public String getErrorLine() {
if (syntaxDetails_ != null) {
return String.valueOf(syntaxDetails_.get(1));
} else {
return "Details unavailable";
}
}
public String getErrorColumn() {
if (syntaxDetails_ != null) {
return String.valueOf(syntaxDetails_.get(2));
} else {
return "Details unavailable";
}
}
public String getErrorOffset() {
if (syntaxDetails_ != null) {
return String.valueOf(syntaxDetails_.get(3));
} else {
return "Details unavailable";
}
}
public String getErrorLength() {
if (syntaxDetails_ != null) {
return String.valueOf(syntaxDetails_.get(4));
} else {
return "Details unavailable";
}
}
public String getErrorText() {
if (syntaxDetails_ != null) {
return String.valueOf(syntaxDetails_.get(5));
} else {
return "Details unavailable";
}
}
}
private transient Session parent_;
private String expression_;
private boolean isValid_;
/**
*
*/
public Formula() {
}
public Formula(final Session parent) {
parent_ = parent;
}
public Formula(final String expression) throws FormulaSyntaxException {
this();
try {
setExpression(expression, true);
} catch (FormulaSyntaxException fe) {
isValid_ = false;
log_.log(Level.WARNING, "Error confirming formula syntax: " + fe.getExpression() + " (" + fe.getErrorText() + ")");
throw fe;
}
}
@Override
public void setSession(final Session session) {
parent_ = session;
}
@Override
public String getExpression() {
return expression_;
}
public Parser getParser() {
if (!isValid_)
return null;
return new Parser(getExpression());
}
public void setExpression(final String expression, final boolean force) {
isValid_ = true;
expression_ = expression;
}
/*
* (non-Javadoc)
*
* @see org.openntf.domino.ext.Formula#setExpression(java.lang.String)
*/
@Override
public void setExpression(String expression) {
if (expression.length() > 2000) {
isValid_ = true;
expression_ = expression;
return;
}
Vector<Object> vec = getSession().evaluate("@CheckFormulaSyntax({" + DominoUtils.escapeForFormulaString(expression) + "})");
if (vec == null) {
isValid_ = false;
} else if (vec.size() > 2) {
isValid_ = false;
} else {
isValid_ = true;
}
if (!isValid_) {
if (decompiler_ != null && !expression.contains("@") && !expression.contains(";")) {//NTF - good chance its compiled
expression = decompiler_.decompileB64(expression);
if (expression != null) {
vec = getSession().evaluate("@CheckFormulaSyntax({" + DominoUtils.escapeForFormulaString(expression) + "})");
if (vec == null || vec.size() > 2) {
isValid_ = false;
throw new FormulaSyntaxException(expression, vec);
} else {
System.out.println("Successfully decompiled a formula!");
isValid_ = true;
}
} else {
throw new FormulaUnableToDecompile(expression);
}
} else {
throw new FormulaSyntaxException(expression, vec);
}
}
if (isValid_) {
expression_ = expression;
}
}
private Session getSession() {
if (parent_ == null) {
// CHECKME RPr: Is that the correct session
parent_ = Factory.getSession(SessionType.CURRENT);
}
return parent_;
}
/*
* (non-Javadoc)
*
* @see org.openntf.domino.ext.Formula#getValue()
*/
@Override
public Vector<Object> getValue() {
if (expression_ == null)
throw new NoFormulaSetException();
Vector<Object> vec = getSession().evaluate(expression_);
return vec;
}
/*
* (non-Javadoc)
*
* @see org.openntf.domino.ext.Formula#getValue(java.lang.Class)
*/
@Override
public <T> T getValue(final Class<T> type) {
Vector<Object> v = getValue();
return TypeUtils.collectionToClass(v, type, getSession());
}
/*
* (non-Javadoc)
*
* @see org.openntf.domino.ext.Formula#getValue()
*/
@Override
public Vector<Object> getValue(final Session session) {
if (expression_ == null)
throw new NoFormulaSetException();
Vector<Object> vec = session.evaluate(expression_);
return vec;
}
/*
* (non-Javadoc)
*
* @see org.openntf.domino.ext.Formula#getValue(java.lang.Class)
*/
@Override
public <T> T getValue(final Session session, final Class<T> type) {
Vector<Object> v = getValue(session);
return TypeUtils.collectionToClass(v, type, session);
}
/*
* (non-Javadoc)
*
* @see org.openntf.domino.ext.Formula#getValue(org.openntf.domino.Document)
*/
@Override
public Vector<Object> getValue(final Document document) {
if (expression_ == null)
throw new NoFormulaSetException();
Vector<Object> vec = document.getAncestorSession().evaluate(expression_, document);
return vec;
}
/*
* (non-Javadoc)
*
* @see org.openntf.domino.ext.Formula#getValue(org.openntf.domino.Document, java.lang.Class)
*/
@Override
public <T> T getValue(final Document document, final Class<T> type) {
Vector<Object> v = getValue(document);
return TypeUtils.collectionToClass(v, type, document.getAncestorSession());
}
/*
* (non-Javadoc)
*
* @see java.io.Externalizable#readExternal(java.io.ObjectInput)
*/
@Override
public void readExternal(final ObjectInput in) throws IOException, ClassNotFoundException {
expression_ = in.readUTF();
}
/*
* (non-Javadoc)
*
* @see java.io.Externalizable#writeExternal(java.io.ObjectOutput)
*/
@Override
public void writeExternal(final ObjectOutput out) throws IOException {
out.writeUTF(expression_);
}
public static class ParserException extends RuntimeException {
private static final long serialVersionUID = 1L;
private final String expression_;
public ParserException(final String message, final String expression) {
super(message);
expression_ = expression;
}
public String getExpression() {
return expression_;
}
}
public static class Parser {
/*
* NTF - Please note that this parser is pretty brutal. It's not intended to construct ASTs from Formula.
* It's intended to allow for analytics of Formula code. So searching for the use of specific @functions,
* literals, keywords, and to try to differentiate local variables from field-based variable references.
*
* A proper AST parser for Formula would be considerably more involved and honestly of questionable value
* since actually having knowledge of the syntax tree would only be useful in an interpretter/converter
* context.
*/
@SuppressWarnings("unused")
private static final Logger log_ = Logger.getLogger(Formula.Parser.class.getName());
public static final String DEFAULT = "DEFAULT";
public static final String REM = "REM";
public static final String ENVIRONMENT = "ENVIRONMENT";
public static final String FIELD = "FIELD";
public static final String LOCAL = "LOCAL";
private final String source_;
private Set<String> functions_;
private Set<String> keywords_;
private Set<String> localVars_;
private Set<String> fieldVars_;
private Set<String> envVars_;
private Set<String> literals_;
private Set<String> numberLiterals_;
private Set<String> variables_;
private Set<String> commands_;
private Stack<String> identStack_;
// private boolean inLiteral_;
// private boolean inBracket_;
// private boolean inEscape_;
private boolean inRightSide_;
@SuppressWarnings("unused")
private Boolean isAssignment_;
@SuppressWarnings("unused")
private boolean justClosedKeyword_;
@SuppressWarnings("unused")
private boolean justClosedExpression_;
private int parenDepth_ = 0;
private char lastOpChar_;
private String curStatementType_;
// private StringBuilder buffer_;
Parser(final String source) {
source_ = source;
}
public void parse() {
parseStatement(source_);
// String[] lines = source_.split("\\n");
// for (String line : lines) {
// parseLine(line);
// }
}
public void parseStatement(final String statement) {
inRightSide_ = false;
curStatementType_ = "";
if (statement != null) {
String result = statement.replaceAll("\\n", "");
result = result.replaceAll("\\r", "").trim();
while (result != null && result.length() > 0) {
// System.out.println("Parsing next statement");
if (result.startsWith(REM)) {
isAssignment_ = Boolean.FALSE;
result = parseComment(result.substring(REM.length()).trim());
} else if (result.startsWith(DEFAULT)) {
result = parseDefaultStatement(result.substring(DEFAULT.length()).trim());
} else if (result.startsWith(ENVIRONMENT)) {
result = parseEnvironmentStatement(result.substring(ENVIRONMENT.length()).trim());
} else if (result.startsWith(FIELD)) {
result = parseFieldStatement(result.substring(FIELD.length()).trim());
} else {
// curStatementType_ = "";
isAssignment_ = null; //we don't know whether this will be an assignment until we see ':='
result = parseNextStatement(result);
// curStatementType_ = "";
}
// inRightSide_ = false;
}
}
}
public String parseDefaultStatement(final String line) {
// System.out.println("Parsing Default");
curStatementType_ = DEFAULT;
isAssignment_ = Boolean.TRUE;
String result = parseNextStatement(line);
return result.trim();
}
public String parseFieldStatement(final String line) {
// System.out.println("Parsing Field");
curStatementType_ = FIELD;
isAssignment_ = Boolean.TRUE;
String result = parseNextStatement(line);
// curStatementType_ = "";
return result.trim();
}
public String parseEnvironmentStatement(final String line) {
// System.out.println("Parsing Environment");
curStatementType_ = ENVIRONMENT;
isAssignment_ = Boolean.TRUE;
String result = parseNextStatement(line);
// curStatementType_ = "";
return result.trim();
}
public String parseComment(final String line) {
// System.out.println("Parsing Comment");
curStatementType_ = REM;
String result = parseNextStatement(line); //we expect this to immediately lead to a literal and then be done
char[] chars = result.toCharArray();
int pos = 0;
for (char c : chars) {
pos++;
if (c == ' ' || c == ';') {
curStatementType_ = "";
return result.substring(pos).trim();
} else {
}
}
// curStatementType_ = "";
System.out.println("End of comment statement reached and no more characters are available");
return "";
}
public String parseNextQuoteLiteral(final String statement) {
boolean inEscape = false;
char[] chars = statement.toCharArray();
int pos = 0;
StringBuilder buffer = new StringBuilder();
for (char c : chars) {
pos++;
if (c == '\\') {
if (inEscape) {
inEscape = false;
buffer.append(c);
// System.out.println("Found escaped quote!");
} else {
inEscape = true;
}
} else if (c == '"') {
if (inEscape) {
inEscape = false;
buffer.append(c);
// System.out.println("Found escaped quote!");
} else {
getLiterals().add(buffer.toString());
return statement.substring(pos);
}
} else {
buffer.append(c);
if (inEscape)
inEscape = false;
}
}
throw new ParserException("End of line reached before end of string literal", statement);
}
public String parseNextBraceLiteral(final String statement) {
// System.out.println("Parsing brace literal");
boolean inEscape = false;
char[] chars = statement.toCharArray();
int pos = 0;
StringBuilder buffer = new StringBuilder();
for (char c : chars) {
pos++;
if (c == '\\') {
if (inEscape) {
buffer.append(c);
inEscape = false;
} else {
inEscape = true;
}
} else if (c == '}') {
if (inEscape) {
buffer.append(c);
inEscape = false;
} else {
getLiterals().add(buffer.toString());
return statement.substring(pos);
}
} else {
buffer.append(c);
if (inEscape)
inEscape = false;
}
}
throw new ParserException("End of line reached before end of string literal", statement);
}
public String parseNextKeyword(final String statement) {
// System.out.println("Parsing Keyword");
char[] chars = statement.toCharArray();
int pos = 0;
StringBuilder buffer = new StringBuilder();
for (char c : chars) {
pos++;
if (c == ']') {
String keyword = buffer.toString();
getKeywords().add(keyword);
// System.out.println("Found keyword: " + keyword);
return statement.substring(pos);
} else {
buffer.append(c);
}
}
throw new ParserException("End of line reached before end of keyword", statement);
}
public String parseNextLiteral(final String statement, final boolean isBrace) {
// System.out.println("Parsing Literal");
if (isBrace) {
return parseNextBraceLiteral(statement);
} else {
try {
return parseNextQuoteLiteral(statement);
} catch (ParserException pe) {
pe.printStackTrace();
System.out.println(pe.getExpression());
return "";
}
}
}
public String parseNextNumberLiteral(final String statement, final char startingDigit) {
char[] chars = statement.toCharArray();
int pos = 0;
StringBuilder buffer = new StringBuilder();
buffer.append(startingDigit);
for (char c : chars) {
pos++;
if (Character.isDigit(c)) {
buffer.append(c);
} else if (c == ';') {
pos--;
break;
} else if (c == ':') {
lastOpChar_ = c;
break;
} else if (c == '(') {
parenDepth_++;
// System.out.println("Opening parens " + parenDepth_);
break;
} else if (c == ')') {
parenDepth_--;
// System.out.println("Closing parens " + parenDepth_);
break;
} else if (c == '[') {
pos--;
break;
} else if (c == ']') {
break;
} else {
break;
}
}
getNumberLiterals().add(buffer.toString());
return statement.substring(pos);
// throw new ParserException("End of line reached before end of number literal", statement);
}
public String parseNextFunction(final String statement) {
// System.out.println("Parsing Function");
char[] chars = statement.toCharArray();
int pos = 0;
StringBuilder buffer = new StringBuilder();
for (char c : chars) {
pos++;
if (c == '(') {
getFunctions().add(buffer.toString());
parenDepth_++;
// System.out.println("Opening parens " + parenDepth_);
return statement.substring(pos);
} else if (c == ')') {
getFunctions().add(buffer.toString());
parenDepth_--;
// System.out.println("Closing parens " + parenDepth_);
return statement.substring(pos);
} else if (c == ' ') {
getFunctions().add(buffer.toString());
return statement.substring(pos);
} else if (c == ';') {
getFunctions().add(buffer.toString());
return statement.substring(pos - 1); //let the calling method handle the end of the statement
} else if (c == '[') {
getFunctions().add(buffer.toString());
return statement.substring(pos - 1);
} else if (c == ']') {
getFunctions().add(buffer.toString());
return statement.substring(pos);
} else {
buffer.append(c);
}
}
String function = buffer.toString();
getFunctions().add(function);
// log_.log(Level.INFO, "Statement ended with a function: " + function);
return "";
}
public String parseNextIdentifier(final String statement, final char startingChar) {
char[] chars = statement.toCharArray();
int pos = 0;
StringBuilder buffer = new StringBuilder();
buffer.append(startingChar);
for (char c : chars) {
pos++;
if (c == ';') {
pos--;
break;
} else if (c == ':') {
lastOpChar_ = c;
break;
} else if (c == '(') {
parenDepth_++;
// System.out.println("Opening parens " + parenDepth_);
break;
} else if (c == ')') {
parenDepth_--;
// System.out.println("Closing parens " + parenDepth_);
break;
} else if (c == '[') {
// parenDepth_++;
pos--;
break;
} else if (c == ']') {
// parenDepth_--;
break;
} else if (isOperator(c)) {
break;
} else if (c == ' ') {
break;
} else {
buffer.append(c);
}
}
String identifier = buffer.toString();
getIdentStack().push(identifier);
if (inRightSide_ && !getLocalVars().contains(identifier)) {
// System.out.println("Adding to field Vars because not in localvar list and we're in the right side");
getFieldVars().add(identifier);
} else {
if (curStatementType_.equals(FIELD)) {
getFieldVars().add(identifier);
} else if (curStatementType_.equals(ENVIRONMENT)) {
getEnvVars().add(identifier);
} else {
// getLocalVars().add(identifier);
}
}
// System.out.println("Completed parsing identifier: " + identifier + " on " + (inRightSide_ ? "right" : "left"));
return statement.substring(pos);
}
public String parseNextStatement(final String segment) {
// System.out.println("Parsing " + curStatementType_ + " Statement Type");
if (segment == null)
return "";
char[] chars = segment.toCharArray();
int pos = 0;
String result = "";
for (char c : chars) {
pos++;
if (c == '{') {
String nextSegment = segment.substring(pos);
result = parseNextLiteral(nextSegment, true);
return result;
} else if (c == '"') {
String nextSegment = segment.substring(pos);
result = parseNextLiteral(nextSegment, false);
return result;
} else if (c == ':') {
lastOpChar_ = c;
} else if (c == '=') {
if (lastOpChar_ == ':') {
//assignment taking place!
// System.out.println("Found assignment!");
inRightSide_ = true;
String lastIdent = getIdentStack().pop();
if (curStatementType_.equals(FIELD)) {
getFieldVars().add(lastIdent);
} else if (curStatementType_.equals(DEFAULT)) {
getLocalVars().add(lastIdent);
} else if (curStatementType_.equals(ENVIRONMENT)) {
getEnvVars().add(lastIdent);
} else if (curStatementType_.length() < 1) {
curStatementType_ = LOCAL;
getLocalVars().add(lastIdent);
}
lastOpChar_ = c;
}
} else if (c == '[') {
String nextSegment = segment.substring(pos);
result = parseNextKeyword(nextSegment);
return result;
} else if (c == '@') {
String nextSegment = segment.substring(pos);
result = parseNextFunction(nextSegment);
return result;
} else if (Character.isLetter(c) || c == '_' || c == '$') {
String nextSegment = segment.substring(pos);
result = parseNextIdentifier(nextSegment, c);
return result;
} else if (Character.isDigit(c)) {
String nextSegment = segment.substring(pos);
result = parseNextNumberLiteral(nextSegment, c);
return result;
} else if (isOperator(c)) {
// System.out.println("Skipping an operator " + c);
} else if (c == '(') {
parenDepth_++;
// System.out.println("Opening parens " + parenDepth_);
} else if (c == ')') {
parenDepth_--;
// System.out.println("Closing parens " + parenDepth_);
} else if (c == '\n') {
// System.out.println("Skipping whitespace");
} else if (c == '\t') {
// System.out.println("Skipping whitespace");
} else if (c == '\r') {
// System.out.println("Skipping whitespace");
} else if (c == ' ') {
// System.out.println("Skipping whitespace");
} else if (c == ';') {
if (parenDepth_ == 0) {
// System.out.println("Statement completed");
inRightSide_ = false;
if (curStatementType_ == null || curStatementType_.length() < 1) {
// System.out.println("Ending result expression");
for (String ident : getIdentStack()) {
if (!getLocalVars().contains(ident)) {
getFieldVars().add(ident); //if the variable was never defined locally, it's most likely a field
}
}
} else {
// System.out.println("Ending " + curStatementType_ + " expression");
}
getIdentStack().clear();
curStatementType_ = "";
return segment.substring(pos).trim();
} else {
// System.out.println("Next argument...");
}
}
}
return result;
}
// public void parseAllSegments(final String statement) {
// char[] chars = statement.toCharArray();
// int pos = 0;
// StringBuilder buffer = new StringBuilder();
// String result = null;
// for (char c : chars) {
// pos++;
// String nextSegment = statement.substring(pos);
//
// }
// }
// public void parseStatement(final String line) {
// char[] chars = line.toCharArray();
// StringBuilder buffer = new StringBuilder();
// int pos = 0;
// for (char c : chars) {
// pos++;
//
// }
//
// }
public static boolean isOperator(final char c) {
return isListOp(c) || isMathOp(c) || isLogicOp(c);
}
public static boolean isListOp(final char c) {
return c == ':';
}
public static boolean isMathOp(final char c) {
return c == '+' || c == '=' || c == '<' || c == '>' || c == '*' || c == '/';
}
public static boolean isLogicOp(final char c) {
return c == '|' || c == '!' || c == '&';
}
/**
* @return the functions
*/
public Set<String> getFunctions() {
if (functions_ == null) {
functions_ = new LinkedHashSet<String>();
}
return functions_;
}
/**
* @return the keywords
*/
public Set<String> getKeywords() {
if (keywords_ == null) {
keywords_ = new LinkedHashSet<String>();
}
return keywords_;
}
/**
* @return the localVars
*/
public Set<String> getLocalVars() {
if (localVars_ == null) {
localVars_ = new LinkedHashSet<String>();
}
return localVars_;
}
/**
* @return the envVars
*/
public Set<String> getEnvVars() {
if (envVars_ == null) {
envVars_ = new LinkedHashSet<String>();
}
return envVars_;
}
/**
* @return the variables
*/
public Set<String> getVariables() {
if (variables_ == null) {
variables_ = new LinkedHashSet<String>();
}
return variables_;
}
/**
* @return the commands
*/
public Set<String> getCommands() {
if (commands_ == null) {
commands_ = new LinkedHashSet<String>();
}
return commands_;
}
public Set<String> getLiterals() {
if (literals_ == null) {
literals_ = new LinkedHashSet<String>();
}
return literals_;
}
public Set<String> getNumberLiterals() {
if (numberLiterals_ == null) {
numberLiterals_ = new LinkedHashSet<String>();
}
return numberLiterals_;
}
public Set<String> getFieldVars() {
if (fieldVars_ == null) {
fieldVars_ = new LinkedHashSet<String>();
}
return fieldVars_;
}
public Stack<String> getIdentStack() {
if (identStack_ == null) {
identStack_ = new Stack<String>();
}
return identStack_;
}
}
}