All Downloads are FREE. Search and download functionalities are using the official Maven repository.

js.template.xhtml.ConditionalExpression Maven / Gradle / Ivy

Go to download

Reference implementation for j(s)-lib template API, declarative, natural and based on X(HT)ML language.

The newest version!
package js.template.xhtml;

import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import js.converter.Converter;
import js.converter.ConverterRegistry;
import js.lang.BugError;
import js.template.TemplateException;
import js.util.Types;

/**
 * Conditional expression evaluator. A conditional expression has a mandatory property path, an operator opcode and an operand.
 * Property path is used to get content value and opcode to enact specific evaluation logic. Evaluation process usually uses two
 * parameters: content value determined by property path and operand from expression.
 * 

* Usage pattern is straightforward: create instance and test expression value. Constructor need templates content and current scope * used to get value to be evaluated and of course expression. * *

 * ConditionalExpression conditionalExpression = new ConditionalExpression(content, scope, expression);
 * if(conditionalExpression.value()) {
 *   // logic executed if conditional expression is true
 * }
 * 
* *

Conditional Expression Syntax

*
 *  conditional-expression = [ not ] property-path [ opcode operand ]
 *  not = '!' ; ! prefix at expression start negate boolean result
 *  property-path = java-name
 *  opcode = '=' / '<' / '>' ; note that opcode cannot be any valid java-name character
 *  java-name = ( 'a'...'z' / '$' / '_' ) *( 'a'...'z' / 'A'...'Z' / '0'...'9' / '$' / '_' )
 * 
* * Here are couple example of working conditional expressions and parsed components. Although only one negated expression is presented please note * that exclamation market prefix can pe used with any operator. *

*

* * * * * * *
Expression * True Condition * Opcode * Property Path * Operand *
flag * flag is a boolean value and is true * NOT_EMPTY * flag * null *
!description * description is a string and is null * NOT_EMPTY * description * null *
type=DIRECTORY * type is an enumeration and its values is DIRECTORY * EQUALS * type * DIRECTORY *
loadedPercent<0.9 * loadedPercent is a double value [0, 1) and its values is less than 0.9 * LESS_THAN * loadedPercent * 0.9 *
birthDay>1980-01-01T00:00:00Z * birthDay is a date and its value is after 1980, January 1-st * GREATER_THAN * birthDay * 1980-01-01T00:00:00Z *
*

* See {@link ConditionalExpression.Opcode} for supported operators. * @author Iulian Rotaru */ final class ConditionalExpression { /** Wrapped string source of this conditional expression, mainly for debugging. */ private String expression; /** * Evaluated value negation flag. If true {@link #evaluate(Object)} applies boolean not on returned value. This * flag is true if expression starts with exclamation mark. */ private boolean not; /** * The property path of the content value to evaluate this conditional expression against. See package API for object * property path description. This value is extracted from given expression and is the only mandatory component. */ private String propertyPath; /** * Optional expression operator opcode, default to {@link ConditionalExpression.Opcode#NOT_EMPTY}. This opcode is used to * select the proper expression {@link Processor}. */ private Opcode opcode = Opcode.NONE; /** * Operator operand, mandatory only if {@link Processor#acceptNullOperand()} requires it. This is the second term of * expression evaluation logic; the first is the content value determined by property path. */ private String operand; /** Expression evaluation value. */ private boolean value = false; /** * Package default constructor. * * @param content dynamic content, * @param scope current object scope, * @param expression conditional expression to parse. * @throws ContentException */ ConditionalExpression(Content content, Object scope, String expression) { this.expression = expression; parse(); this.value = this.evaluate(content.getObject(scope, this.propertyPath)); } /** * Return this conditional expression boolean value. * * @return this conditional expression value. */ public boolean value() { return this.value; } /** * Parse conditional expression string and return the property path. This method is in fact a morphological parser, i.e. a * lexer. It just identifies expression components and initialize internal state. Does not check validity; all * insanity tests are performed by {@link #evaluate(Object)} counterpart. Returns property path expression * component, which is in fact the only mandatory part. */ private void parse() { if (this.expression.charAt(0) == '!') { this.not = true; this.expression = this.expression.substring(1); } StringBuilder sb = new StringBuilder(); State state = State.PROPERTY_PATH; for (int i = 0; i < this.expression.length(); ++i) { char c = this.expression.charAt(i); switch (state) { case PROPERTY_PATH: if (isPropertyPathChar(c)) { sb.append(c); break; } this.propertyPath = sb.toString(); sb.setLength(0); this.opcode = Opcode.forChar(c); state = State.OPERAND; break; case OPERAND: sb.append(c); break; default: throw new IllegalStateException(); } } if (state == State.PROPERTY_PATH) { assert this.opcode == Opcode.NONE; this.propertyPath = sb.toString(); this.opcode = Opcode.NOT_EMPTY; } else { if (sb.length() > 0) { // operand string builder may be empty if operand is missing, e.g. 'value=' this.operand = sb.toString(); } } } private static boolean isPropertyPathChar(char c) { return c == '.' || Character.isJavaIdentifierPart(c); } /** * Evaluate this conditional expression against given object value. Execute this conditional expression operator on given * object value and {@link #operand} defined by expression. Evaluation is executed after {@link #parse()} * counter part that already initialized this conditional expression internal state. This method takes care to test internal * state consistency and throws templates exception if bad. * * @param object value to evaluate. * @return true if this conditional expression is positively evaluated. * @throws TemplateException if this conditional expression internal state is not consistent. */ private boolean evaluate(Object object) { if (this.opcode == Opcode.INVALID) { throw new TemplateException("Invalid conditional expression |%s|. Not supported opcode.", this.expression); } Processor processor = getProcessor(opcode); if (this.operand == null && !processor.acceptNullOperand()) { throw new TemplateException("Invalid conditional expression |%s|. Missing mandatory operand for operator |%s|.", this.expression, this.opcode); } if (!processor.acceptValue(object)) { throw new TemplateException("Invalid conditional expression |%s|. Operator |%s| does not accept value type |%s|.", this.expression, this.opcode, object.getClass()); } if (this.operand != null && !OperandFormatValidator.isValid(object, this.operand)) { throw new TemplateException("Invalid conditional expression |%s|. Operand does not match value type |%s|. See |%s| API.", this.expression, object.getClass(), OperandFormatValidator.class); } boolean value = processor.evaluate(object, this.operand); return this.not ? !value : value; } /** * Parser state machine. * * @author Iulian Rotaru */ private static enum State { /** * Neutral value. */ NONE, /** * Building property path. */ PROPERTY_PATH, /** * Building operand. */ OPERAND } /** * Operator opcodes supported by current conditional expression implementation. Operators always operates on content value * identified by {@link ConditionalExpression#propertyPath} and optional {@link ConditionalExpression#operand}. * * * @author Iulian Rotaru */ private static enum Opcode { /** * Neutral value. */ NONE, /** * Invalid character code. Parser uses this opcode when discover a not supported character code for opcode. */ INVALID, /** * Value if not empty. A value is empty if is null, empty string, boolean false, zero value number, collection or array * with zero size. It is implemented by {@link NotEmptyProcessor}. */ NOT_EMPTY, /** * Value and operand are equal. It is implemented by {@link EqualsProcessor}. */ EQUALS, /** * Value is strictly less than operand. It is implemented by {@link LessThanProcessor}. */ LESS_THAN, /** * Value is strictly greater than operand. It is implemented by {@link GreaterThanProcessor}. */ GREATER_THAN; /** * Returns the opcode encoded by given character code. Current implementation encode opcode with a single character. If * given code is not supported returns {@link ConditionalExpression.Opcode#INVALID}. * * @param code opcode character code. * @return opcode specified by given code or INVALID. */ public static Opcode forChar(char code) { switch (code) { case '=': return EQUALS; case '<': return LESS_THAN; case '>': return GREATER_THAN; } return INVALID; } } /** * Every conditional expression operator implements this processor interface. A processor implements the actual evaluation * logic, see {@link #evaluate(Object, String)}. Evaluation always occurs on a content value designated by property path and * an optional operand, both described by conditional expression. Value is always first and is important on order based * operators, e.g. on LESS_THAN value should be less than operand. *

* Operand can miss in which case evaluation consider only the value, for example, NOT_EMPTY test value emptiness. *

* Processor interface provides also predicates to test if implementation supports null operand and if certain value is * acceptable for processing. * * @author Iulian Rotaru */ private static interface Processor { /** * Apply evaluation specific logic to given value and optional operand. * * @param value value to evaluate, possible null, * @param operand optional operand to evaluate value against, default to null. * @return evaluation logic result. */ boolean evaluate(Object value, String operand); /** * Test if processor implementation accepts null operand. It is a templates exception if operator processor does not * accept null operand and expression do not include it. * * @return true if this processor accepts null operand. */ boolean acceptNullOperand(); /** * Test performed just before evaluation to determine if given value can be processed. Most common usage is to consider * value type; for example LESS_THAN operator cannot handle boolean values. * * @param value value to determine if processable. * @return true if given value can be evaluated by this processor. */ boolean acceptValue(Object value); } /** NOT_EMPTY operator processor instance. */ private static final Processor NOT_EPMTY_PROCESSOR = new NotEmptyProcessor(); /** EQUALS operator processor instance. */ private static Processor EQUALS_PROCESSOR; /** LESS_THAN operator processor instance. */ private static Processor LESS_THAN_PROCESSOR; /** GREATER_THAN operator processor instance. */ private static Processor GREATER_THAN_PROCESSOR; /** * Operator processor factory. Returned processor instance is a singleton, that is, reused on running virtual machine. * * @param opcode return processor suitable for requested operator. * @return operator processor instance. */ private static Processor getProcessor(Opcode opcode) { switch (opcode) { case NOT_EMPTY: return NOT_EPMTY_PROCESSOR; case EQUALS: if (EQUALS_PROCESSOR == null) { EQUALS_PROCESSOR = new EqualsProcessor(); } return EQUALS_PROCESSOR; case LESS_THAN: if (LESS_THAN_PROCESSOR == null) { LESS_THAN_PROCESSOR = new LessThanProcessor(); } return LESS_THAN_PROCESSOR; case GREATER_THAN: if (GREATER_THAN_PROCESSOR == null) { GREATER_THAN_PROCESSOR = new GreaterThanProcessor(); } return GREATER_THAN_PROCESSOR; default: throw new BugError("Unsupported opcode |%s|.", opcode); } } /** * Operator processor for not empty value test. * * @author Iulian Rotaru */ private static final class NotEmptyProcessor implements Processor { @Override public boolean evaluate(Object value, String operand) { return Types.asBoolean(value) == true; } @Override public boolean acceptNullOperand() { return true; } @Override public boolean acceptValue(Object value) { return true; } } /** * Equality operator processor. * * @author Iulian Rotaru */ private static final class EqualsProcessor implements Processor { /** * Implements equality test logic. This method converts value to string using {@link Converter} then compare it with * operand. As a consequence operand format must be compatible with value type. For example if value type is a * {@link Date} operand syntax should be ISO8601; please see {@link Converter} documentation for supported formats. */ @Override public boolean evaluate(Object value, String operand) { if (value == null) { return operand.equals("null"); } if (value instanceof Date) { return evaluateDates((Date) value, operand); } return ConverterRegistry.getConverter().asString(value).equals(operand); } private static boolean evaluateDates(Date date, String dateFormat) { int[] dateItems = Dates.dateItems(date); Matcher matcher = Dates.dateMatcher(dateFormat); for (int i = 0; i < dateItems.length; ++i) { String value = matcher.group(i + 1); if (value == null) { break; } if (dateItems[i] != Integer.parseInt(value)) { return false; } } return true; } @Override public boolean acceptNullOperand() { return false; } @Override public boolean acceptValue(Object vlaue) { return true; } } /** * Base class for inequality comparisons. * * @author Iulian Rotaru */ private static abstract class ComparisonProcessor implements Processor { @Override public boolean evaluate(Object value, String operand) { if (Types.isNumber(value)) { Converter converter = ConverterRegistry.getConverter(); Double doubleValue = ((Number) value).doubleValue(); Double doubleOperand = 0.0; if (value instanceof Float) { // converting float to double may change last decimal digits // we need to ensure both value and operand undergo the same treatment, i.e. use Float#doubleValue() method // for both // for example float number 1.23F is converted to double to 1.2300000190734863 // if we convert string "1.23" to double we have 1.23 != 1.2300000190734863 Float floatOperand = converter.asObject(operand, Float.class); doubleOperand = floatOperand.doubleValue(); } else { doubleOperand = converter.asObject(operand, Double.class); } return compare(doubleValue, doubleOperand); } if (Types.isDate(value)) { Date dateValue = (Date) value; Date dateOperand = Dates.parse(operand); return compare(dateValue, dateOperand); } return false; } /** * Comparator for numbers. * * @param value numeric value, * @param operand numeric operand. * @return true if value and operand fulfill comparator criterion. */ protected abstract boolean compare(Double value, Double operand); /** * Comparator for calendar dates. * * @param value date value, * @param operand date operand. * @return true if value and operand fulfill comparator criterion. */ protected abstract boolean compare(Date value, Date operand); /** * Comparison processors always require not null operand. */ @Override public boolean acceptNullOperand() { return false; } /** * Current implementation of comparison processor accept numbers and dates. */ @Override public boolean acceptValue(Object value) { if (Types.isNumber(value)) { return true; } if (Types.isDate(value)) { return true; } return false; } } /** * Comparison processor implementation for LESS_THAN operator. * * @author Iulian Rotaru */ private static class LessThanProcessor extends ComparisonProcessor { @Override protected boolean compare(Double value, Double operand) { return value < operand; } @Override protected boolean compare(Date value, Date operand) { return value.compareTo(operand) < 0; } } /** * Comparison processor implementation for GREATER_THAN operator. * * @author Iulian Rotaru */ private static final class GreaterThanProcessor extends ComparisonProcessor { @Override protected boolean compare(Double value, Double operand) { return value > operand; } @Override protected boolean compare(Date value, Date operand) { return value.compareTo(operand) > 0; } } /** * Utility class for operand format validation. Operand is a string and has a specific format that should be compatible with * value type. For example if value is a date operand should be ISO8601 date format. A null operand is not compatible with * any value. *

* Current validator implementation recognizes boolean, number and date types. All other value types are not on scope of * this validator and always return positive. For supported types see this class regular expression patterns. * * @author Iulian Rotaru */ private static final class OperandFormatValidator { /** * Date format should be ISO8601 with UTC time zone, dddd-dd-ddTdd:dd:ddZ. */ private static final Pattern DATE_PATTERN = Pattern.compile("\\d{4}(?:-\\d{2}(?:-\\d{2}(?:T\\d{2}(?::\\d{2}(?::\\d{2}(?:Z)?)?)?)?)?)?"); /** * Signed numeric decimal number but not scientific notation. */ private static final Pattern NUMBER_PATTERN = Pattern.compile("[+-]?\\d+(?:\\.\\d+)?"); /** * Boolean operand should be true or false, lower case. */ private static final Pattern BOOLEAN_PATTERN = Pattern.compile("true|false"); /** * Private constructor. */ private OperandFormatValidator() { } /** * Check operand format compatibility against value counterpart. * * @param value content value to validate operand against, * @param operand formatted string operand. * @return true if operand format is compatible with requested value. */ public static boolean isValid(Object value, String operand) { if (operand == null) { // validation is enacted only for operator processors that do not accept null operand // so always return false return false; } if (Types.isBoolean(value)) { return BOOLEAN_PATTERN.matcher(operand).matches(); } if (Types.isNumber(value)) { return NUMBER_PATTERN.matcher(operand).matches(); } if (Types.isDate(value)) { return DATE_PATTERN.matcher(operand).matches(); } return true; } } /** * Utility class for dates related processing. * * @author Iulian Rotaru */ private static final class Dates { /** * ISO8601 date format pattern but with optional fields. Only year is mandatory. Optional fields should be in sequence; * if an optional field is missing all others after it should also be missing. */ private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4})(?:-(\\d{2})(?:-(\\d{2})(?:T(\\d{2})(?::(\\d{2})(?::(\\d{2})(?:Z)?)?)?)?)?)?"); /** * Private constructor. */ private Dates() { } /** * Get a regular expression matcher for ISO8601 date format but with optional fields. * * @param dateFormat date format to parse. * @return matcher initialized with values from given dateFormat. */ public static Matcher dateMatcher(String dateFormat) { Matcher matcher = DATE_PATTERN.matcher(dateFormat); if (!matcher.matches()) { throw new IllegalStateException(); } return matcher; } /** * Parse ISO8601 formatted date but accept optional fields. For missing fields uses sensible default values, that is, * minimum value specific to field. For example, default day of the month value is 1. * * @param dateFormat date format to parse. * @return date instance initialized from given dateFormat. */ public static Date parse(String dateFormat) { Matcher matcher = dateMatcher(dateFormat); Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); calendar.set(Calendar.YEAR, group(matcher, 1)); calendar.set(Calendar.MONTH, group(matcher, 2)); calendar.set(Calendar.DAY_OF_MONTH, group(matcher, 3)); calendar.set(Calendar.HOUR_OF_DAY, group(matcher, 4)); calendar.set(Calendar.MINUTE, group(matcher, 5)); calendar.set(Calendar.SECOND, group(matcher, 6)); return calendar.getTime(); } /** * Return normalized integer value for specified matcher group. If requested group value is null uses sensible default * values. Takes care to return 0 for January and 1 for default day of the month; all other fields defaults to 0. * * @param matcher regular expression matcher, * @param group group number. * @return integer value extracted from matcher or specific default value. */ private static int group(Matcher matcher, int group) { String value = matcher.group(group); if (group == 2) { // the second group is hard coded to month and should be normalized, January should be 0 return parseInt(value, 1) - 1; } if (group == 3) { // the third group is hard coded to day of month and should default to 1 return parseInt(value, 1); } // all other groups defaults to 0 return parseInt(value, 0); } /** * Return integer value from given numeric string or given default if value is null. * * @param value numeric value, possible null, * @param defaultValue default value used when value is null. * @return integer value, parsed or default. */ private static int parseInt(String value, int defaultValue) { return value != null ? Integer.parseInt(value) : defaultValue; } /** * Decompose date into its constituent items. Takes care to human normalize month value, that is, January is 1 not 0. * * @param date date value. * @return given date items. */ public static int[] dateItems(Date date) { Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); calendar.setTime(date); int[] items = new int[6]; items[0] = calendar.get(Calendar.YEAR); items[1] = calendar.get(Calendar.MONTH) + 1; items[2] = calendar.get(Calendar.DAY_OF_MONTH); items[3] = calendar.get(Calendar.HOUR_OF_DAY); items[4] = calendar.get(Calendar.MINUTE); items[5] = calendar.get(Calendar.SECOND); return items; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy