package org.sigmah.shared.computation;
/*
* #%L
* Sigmah
* %%
* Copyright (C) 2010 - 2016 URD
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/
import org.sigmah.shared.computation.dependency.CollectionDependency;
import org.sigmah.shared.computation.dependency.ContributionDependency;
import org.sigmah.shared.computation.dependency.Dependency;
import org.sigmah.shared.computation.dependency.Scope;
import org.sigmah.shared.computation.instruction.BadVariable;
import org.sigmah.shared.computation.instruction.Function;
import org.sigmah.shared.computation.instruction.Instructions;
import org.sigmah.shared.computation.instruction.Operator;
import org.sigmah.shared.computation.instruction.ReduceFunction;
import org.sigmah.shared.computation.instruction.ScopeFunction;
import org.sigmah.shared.computation.instruction.Variable;
import org.sigmah.shared.dto.element.FlexibleElementDTO;
/**
* States of the <code>Computation</code> parser.
*
* @author Raphaƫl Calabro (raphael.calabro@netapsys.fr)
* @since 2.1
*/
enum ParserState {
/**
* Waiting for an operand. Initial state of the parser.
*/
WAITING_FOR_OPERAND {
@Override
int execute(final int offset, final char[] array, final ParserEnvironment environment) {
for (int index = offset; index < array.length; index++) {
final char c = array[index];
if (isDigit(c)) {
environment.setState(CONSTANT);
return index;
} else if (c == MINUS_ALIAS) {
environment.setState(UNARY_OPERATOR);
return index;
} else if (c == LEFT_PARENTHESIS) {
environment.pushContext();
} else if (c == RIGHT_PARENTHESIS) {
environment.popContext();
} else if (c == ID_MARK) {
environment.setState(VARIABLE_ID);
return index;
} else if (isLetter(c) || c == '_') {
environment.setState(VARIABLE_CODE);
return index;
} else if (!isSpace(c)) {
throw new IllegalArgumentException("Formula '"
+ new String(array)
+ "' is unparseable. Got bad element '" + c
+ "', accepted elements are: "
+ "[0-9], "
+ MINUS_ALIAS + ", "
+ LEFT_PARENTHESIS + ", "
+ RIGHT_PARENTHESIS + ", "
+ ID_MARK + ", "
+ "[a-zA-Z] or space.");
}
}
return array.length;
}
},
/**
* Reading a constant value.
*/
CONSTANT {
@Override
int execute(final int offset, final char[] array, final ParserEnvironment environment) {
final StringBuilder constantBuilder = new StringBuilder();
for (int index = offset; index < array.length; index++) {
final char c = array[index];
if (isDigit(c)) {
constantBuilder.append(c);
} else if (isDecimalMark(c)) {
constantBuilder.append(DECIMAL_MARK);
} else {
addConstant(constantBuilder.toString(), environment);
environment.setState(WAITING_FOR_OPERATOR);
return index;
}
}
addConstant(constantBuilder.toString(), environment);
return array.length;
}
},
/**
* Reading a flexible element identifier.
*/
VARIABLE_ID {
@Override
int execute(final int offset, final char[] array, final ParserEnvironment environment) {
final StringBuilder idBuilder = new StringBuilder();
for (int index = offset; index < array.length; index++) {
final char c = array[index];
if (isDigit(c) || c == ID_MARK) {
idBuilder.append(c);
} else {
addVariable(idBuilder.toString(), environment);
environment.setState(WAITING_FOR_OPERATOR);
return index;
}
}
addVariable(idBuilder.toString(), environment);
return array.length;
}
},
/**
* Reading a flexible element code.
*/
VARIABLE_CODE {
@Override
int execute(final int offset, final char[] array, final ParserEnvironment environment) {
final StringBuilder codeBuilder = new StringBuilder();
for (int index = offset; index < array.length; index++) {
final char c = array[index];
if (isLetter(c) || isDigit(c) || c == '_') {
codeBuilder.append(c);
} else if (c == LEFT_PARENTHESIS) {
final Function function = Instructions.getFunctionNamed(codeBuilder.toString());
environment.setLastFunction(function);
environment.setState(WAITING_FOR_FUNCTION_ARGUMENT);
return index + 1;
} else {
addVariable(codeBuilder.toString(), environment);
environment.setState(WAITING_FOR_OPERATOR);
return index;
}
}
addVariable(codeBuilder.toString(), environment);
return array.length;
}
},
/**
* Waiting for the argument of a function.
*/
WAITING_FOR_FUNCTION_ARGUMENT {
@Override
int execute(final int offset, final char[] array, final ParserEnvironment environment) {
final StringBuilder argumentBuilder = new StringBuilder();
for (int index = offset; index < array.length; index++) {
final char c = array[index];
if (c == RIGHT_PARENTHESIS) {
final Function function = environment.popLastFunction();
final String argument = toStringOrNull(argumentBuilder);
if (function instanceof ScopeFunction) {
final ScopeFunction scopeFunction = (ScopeFunction) function;
scopeFunction.setModelName(argument);
environment.setCurrentScope(scopeFunction.toScope());
environment.setState(WAITING_FOR_REDUCE_FUNCTION);
}
else if (function instanceof ReduceFunction) {
if (argument == null) {
throw new IllegalArgumentException("Argument is mandatory for reduce functions.");
}
final Scope scope = environment.getCurrentScope();
final Dependency dependency;
if (ContributionDependency.REFERENCE.equals(argument)) {
dependency = new ContributionDependency(scope);
} else {
if (scope.getModelName() == null) {
throw new IllegalArgumentException("Project model is mandatory for field codes.");
}
dependency = new CollectionDependency(scope, argument);
}
environment.add(new Variable(dependency));
environment.add(function);
environment.setState(WAITING_FOR_OPERATOR);
}
return index + 1;
} else {
argumentBuilder.append(c);
}
}
return array.length;
}
},
/**
* Waiting for a reduce function (like average or sum).
*/
WAITING_FOR_REDUCE_FUNCTION {
@Override
int execute(int offset, char[] array, ParserEnvironment environment) {
for (int index = offset; index < array.length; index++) {
final char c = array[index];
if (c == '.') {
environment.setState(VARIABLE_CODE);
return index + 1;
}
}
return array.length;
}
},
/**
* Waiting for an operator.
*/
WAITING_FOR_OPERATOR {
@Override
int execute(final int offset, final char[] array, final ParserEnvironment environment) {
for (int index = offset; index < array.length; index++) {
final char c = array[index];
if (c == RIGHT_PARENTHESIS) {
environment.popContext();
} else if (c == LEFT_PARENTHESIS && environment.lastInstruction() instanceof BadVariable) {
throw new UnsupportedOperationException("Functions are not supported yet.");
} else if (!isSpace(c)) {
environment.setState(OPERATOR);
return index;
}
}
return array.length;
}
},
/**
* Reading an operator.
*/
OPERATOR {
@Override
int execute(final int offset, final char[] array, final ParserEnvironment environment) {
final StringBuilder operatorBuilder = new StringBuilder();
for (int index = offset; index < array.length; index++) {
final char c = array[index];
if (!isSpace(c) && !isDigit(c) && c != LEFT_PARENTHESIS) {
operatorBuilder.append(c);
} else {
pushOperator(operatorBuilder.toString(), environment);
environment.setState(WAITING_FOR_OPERAND);
return index;
}
}
pushOperator(operatorBuilder.toString(), environment);
return array.length;
}
},
/**
* Reading an unary operator.
*/
UNARY_OPERATOR {
@Override
int execute(final int offset, final char[] array, final ParserEnvironment environment) {
if (array[offset] == MINUS_ALIAS) {
pushOperator(MINUS_OPERATOR, environment);
}
environment.setState(WAITING_FOR_OPERAND);
return offset + 1;
}
};
private static final char LEFT_PARENTHESIS = '(';
private static final char RIGHT_PARENTHESIS = ')';
private static final char DECIMAL_MARK = '.';
private static final char ID_MARK = Instructions.ID_PREFIX;
private static final char MINUS_ALIAS = '-';
private static final String MINUS_OPERATOR = "minus";
/**
* Execute the current state.
*
* @param offset Where to start the analysis.
* @param array Rule to parse.
* @param environment Environment.
* @return New offset where to continue the analysis.
*/
abstract int execute(int offset, char[] array, ParserEnvironment environment);
/**
* Returns <code>true</code> if the given character is a digit.
*
* @param c Character to test.
* @return <code>true</code> if the given character is a digit,
* <code>false</code> otherwise.
*/
private static boolean isDigit(final char c) {
return c >= '0' && c <= '9';
}
/**
* Returns <code>true</code> if the given character is a decimal mark.
*
* @param c Character to test.
* @return <code>true</code> if the given character is a decimal mark,
* <code>false</code> otherwise.
*/
private static boolean isDecimalMark(final char c) {
return c == ',' || c == '.';
}
/**
* Returns <code>true</code> if the given character is a space.
*
* @param c Character to test.
* @return <code>true</code> if the given character is a space,
* <code>false</code> otherwise.
*/
private static boolean isSpace(final char c) {
return c == ' ';
}
/**
* Returns <code>true</code> if the given character is a letter.
*
* @param c Character to test.
* @return <code>true</code> if the given character is a letter,
* <code>false</code> otherwise.
*/
private static boolean isLetter(final char c) {
return Character.isLetter(c);
}
/**
* Adds the given constant to the instructions.
*
* @param constant Constant to add.
* @param environment Environment.
*/
private static void addConstant(final String constant, final ParserEnvironment environment) {
if (constant.length() > 0) {
environment.add(Instructions.getConstantWithValue(constant));
}
}
/**
* Adds the given variable to the instructions.
*
* @param variable Variable to add.
* @param environment Environment.
*/
private static void addVariable(final String variable, final ParserEnvironment environment) {
final FlexibleElementDTO element = environment.getElement(variable);
if (element != null) {
environment.add(new Variable(element));
} else {
environment.add(new BadVariable(variable));
}
}
private static String toStringOrNull(final StringBuilder stringBuilder) {
final String content = stringBuilder.toString().trim();
if (!content.isEmpty()) {
return content;
} else {
return null;
}
}
/**
* Push the given operator to the operator stack. It will be added to the
* instructions after reading the next operator and if its priority is
* superior.
*
* @param operator Operator to push.
* @param environment Environment.
*/
private static void pushOperator(final String operatorName, final ParserEnvironment environment) {
final Operator operator = Instructions.getOperatorNamed(operatorName);
if (operator != null) {
final int parentPriority = environment.peekOfStack() == null ? -1 : environment.peekOfStack().getPriority().ordinal();
final int thisPriority = operator.getPriority().ordinal() - 1;
if(parentPriority > thisPriority) {
environment.add(environment.popFromStack());
}
environment.pushOnStack(operator);
}
else {
throw new IllegalArgumentException("Operator '" + operatorName + "' is invalid.");
}
}
}