/*
* Copyright 2015 Martin Kouba
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.trimou.handlebars;
import static org.trimou.handlebars.OptionsHashKeys.OPERATOR;
import static org.trimou.handlebars.OptionsHashKeys.OUTPUT;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.math.BigInteger;
import org.trimou.exception.MustacheException;
import org.trimou.exception.MustacheProblem;
import org.trimou.handlebars.HelperDefinition.ValuePlaceholder;
/**
* A simple numeric expression helper. During evaluation all the params are
* converted to {@link BigDecimal}s. For the list of supported operators see the
* {@link Operator} enum values, e.g.:
*
* <pre>
* {{numExpr val op="neg" out='It is a negative number!'}}
* {{#numExpr val "90" op="gt"}}
* val > 90
* {{/numExpr}}
* {{#numExpr val 10 op="eq"}}
* val == 10
* {{/numExpr}}
* {{#numExpr val1 val2 op="le"}}
* val1 <= val2
* {{/numExpr}}
* {{#numExpr val "1" 2 '3' op="in"}}
* val = 1 or val = 2 or val = 3
* {{/numExpr}}
* </pre>
*
* <p>
* It's also possible to specify the default operator ({@link Operator#EQ} by
* default) so that the <code>op</code> param may be ommitted:
* </p>
*
* <pre>
* {{#gt val1 10}} val1 > 10 {{/gt}}
* </pre>
*
* <p>
* Sometimes it also makes sense to register the helper with a name derived from
* the default operator or even register a helper instance for each operator -
* see {@link #forEachOperator()}.
* </p>
*
* @author Martin Kouba
*/
public class NumericExpressionHelper extends BasicHelper {
private final Operator defaultOperator;
/**
* {@link Operator#toString()} is used the helper name.
*
* @return a builder instance with {@link NumericExpressionHelper} instance
* registered for each {@link Operator}
*/
public static HelpersBuilder forEachOperator() {
HelpersBuilder builder = HelpersBuilder.empty();
for (Operator operator : Operator.values()) {
builder.add(operator.toString().toLowerCase(),
new NumericExpressionHelper(operator));
}
return builder;
}
/**
*
*/
public NumericExpressionHelper() {
this(Operator.EQ);
}
/**
*
* @param defaultOperator
*/
public NumericExpressionHelper(Operator defaultOperator) {
this.defaultOperator = defaultOperator;
}
@Override
public void execute(Options options) {
Operator operator = initOperator(options);
if (operator.getMinParams() > options.getParameters().size()) {
// We need this check because the operator may be set dynamically
throw new MustacheException(
MustacheProblem.RENDER_HELPER_INVALID_OPTIONS,
"More parameters required [helper: %s, template: %s, line: %s]",
NumericExpressionHelper.class.getName(),
options.getTagInfo().getTemplateName(),
options.getTagInfo().getLine());
}
boolean result = operator.evaluate(options);
if (result) {
if (isSection(options)) {
options.fn();
} else {
String output;
Object outputValue = options.getHash().get(OUTPUT);
output = outputValue != null ? convertValue(outputValue)
: Boolean.TRUE.toString();
append(options, output);
}
}
}
@Override
public void validate(HelperDefinition definition) {
super.validate(definition);
Operator operator;
Object value = definition.getHash().get(OPERATOR);
if (value == null) {
operator = Operator.EQ;
} else if (value instanceof ValuePlaceholder) {
// Operator set dynamically
operator = null;
} else {
operator = Operator.from(value.toString());
}
if (operator != null && operator.getMinParams() > definition
.getParameters().size()) {
throw new MustacheException(
MustacheProblem.COMPILE_HELPER_VALIDATION_FAILURE,
"Operator requires more parameters [helper: %s, template: %s, line: %s]",
this.getClass().getName(),
definition.getTagInfo().getTemplateName(),
definition.getTagInfo().getLine());
}
}
private Operator initOperator(Options options) {
Operator operator = null;
Object value = options.getHash().get(OPERATOR);
if (value != null) {
operator = Operator.from(value.toString());
}
return operator != null ? operator : defaultOperator;
}
private static BigDecimal getDecimal(int index, Options options) {
return getDecimal(options.getParameters().get(index), options);
}
static BigDecimal getDecimal(Object value, Options options) {
BigDecimal decimal;
if (value instanceof BigDecimal) {
decimal = (BigDecimal) value;
} else if (value instanceof BigInteger) {
decimal = new BigDecimal((BigInteger) value);
} else if (value instanceof Long) {
decimal = new BigDecimal((Long) value);
} else if (value instanceof Integer) {
decimal = new BigDecimal((Integer) value);
} else if (value instanceof Double) {
decimal = new BigDecimal((Double) value);
} else if (value instanceof String) {
decimal = new BigDecimal(value.toString());
} else {
throw new MustacheException(
MustacheProblem.RENDER_HELPER_INVALID_OPTIONS,
"Parameter is not valid [param: %s, helper: %s, template: %s, line: %s]",
value, NumericExpressionHelper.class.getName(),
options.getTagInfo().getTemplateName(),
options.getTagInfo().getLine());
}
return decimal;
}
/**
*
* @author Martin Kouba
*/
public enum Operator {
/**
* Evaluates to true if the first and the second value are equal in
* value.
*
* @see BigDecimal#compareTo(BigDecimal)
*/
EQ(new EqualsEvaluator()),
/**
* Evaluates to true if the first and the second value are NOT equal in
* value.
*
* @see BigDecimal#compareTo(BigDecimal)
*/
NEQ(new InverseEvaluator(new EqualsEvaluator())),
/**
* Evaluates to true if the first value is greater than the second
* value.
*
* @see BigDecimal#compareTo(BigDecimal)
*/
GT(options -> {
BigDecimal val1 = getDecimal(0, options);
BigDecimal val2 = getDecimal(1, options);
return val1.compareTo(val2) > 0;
}),
/**
* Evaluates to true if the first value is greater than or equal to the
* second value.
*
* @see BigDecimal#compareTo(BigDecimal)
*/
GE(options -> {
BigDecimal val1 = getDecimal(0, options);
BigDecimal val2 = getDecimal(1, options);
return val1.compareTo(val2) >= 0;
}),
/**
* Evaluates to true if the first value is less than the second value.
*
* @see BigDecimal#compareTo(BigDecimal)
*/
LT(options -> {
BigDecimal val1 = getDecimal(0, options);
BigDecimal val2 = getDecimal(1, options);
return val1.compareTo(val2) < 0;
}),
/**
* Evaluates to true if the first value is less than or equal to the
* second value.
*
* @see BigDecimal#compareTo(BigDecimal)
*/
LE(options -> {
BigDecimal val1 = getDecimal(0, options);
BigDecimal val2 = getDecimal(1, options);
return val1.compareTo(val2) <= 0;
}),
/**
* Evaluates to true if the first value is negative.
*/
NEG(1, options -> getDecimal(0, options).compareTo(BigDecimal.ZERO) < 0),
/**
* Evaluates to true if the first value is positive.
*/
POS(1, options -> getDecimal(0, options).compareTo(BigDecimal.ZERO) > 0),
/**
* Evaluates to true if the first value is found in the set of other
* values. Elements of {@link Iterable}s and arrays are treated as
* separate objects.
*/
IN(new InEvaluator()),
/**
* Evaluates to true if the first value is not found in the set of other
* values. Elements of {@link Iterable}s and arrays are treated as
* separate objects.
*/
NIN(new InverseEvaluator(new InEvaluator())),;
Operator(Evaluator evaluator) {
this(2, evaluator);
}
Operator(int minParams, Evaluator evaluator) {
this.minParams = minParams;
this.evaluator = evaluator;
}
private final int minParams;
private final Evaluator evaluator;
public int getMinParams() {
return minParams;
}
public boolean evaluate(Options options) {
return evaluator.evaluate(options);
}
static Operator from(String value) {
if (value != null) {
for (Operator operator : values()) {
if (operator.toString().equalsIgnoreCase(value)) {
return operator;
}
}
}
return null;
}
}
interface Evaluator {
boolean evaluate(Options options);
}
private static final class InEvaluator implements Evaluator {
@Override
public boolean evaluate(Options options) {
BigDecimal val = getDecimal(0, options);
for (int i = 1; i < options.getParameters().size(); i++) {
Object toTest = options.getParameters().get(i);
if (toTest == null) {
continue;
}
if (toTest instanceof Iterable) {
for (final Object o : ((Iterable<?>) toTest)) {
if (test(val, getDecimal(o, options))) {
return true;
}
}
} else if (toTest.getClass().isArray()) {
int length = Array.getLength(toTest);
for (int j = 0; j < length; j++) {
if (test(val,
getDecimal(Array.get(toTest, j), options))) {
return true;
}
}
} else {
if (test(val, getDecimal(toTest, options))) {
return true;
}
}
}
return false;
}
private boolean test(BigDecimal val1, BigDecimal val2) {
return val1.compareTo(val2) == 0;
}
}
private static class EqualsEvaluator implements Evaluator {
@Override
public boolean evaluate(Options options) {
BigDecimal val1 = getDecimal(0, options);
BigDecimal val2 = getDecimal(1, options);
return val1.compareTo(val2) == 0;
}
}
private static class InverseEvaluator implements Evaluator {
private final Evaluator evaluator;
InverseEvaluator(Evaluator evaluator) {
this.evaluator = evaluator;
}
@Override
public boolean evaluate(Options options) {
return !evaluator.evaluate(options);
}
}
}