blob: f6089e13fa15619407d6e70deb527154316506bd [file] [log] [blame]
/*******************************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.ofbiz.base.util.string;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import javax.el.PropertyNotFoundException;
import org.ofbiz.base.lang.IsEmpty;
import org.ofbiz.base.lang.SourceMonitored;
import org.ofbiz.base.util.Debug;
import org.ofbiz.base.util.ObjectType;
import org.ofbiz.base.util.ScriptUtil;
import org.ofbiz.base.util.UtilDateTime;
import org.ofbiz.base.util.UtilFormatOut;
import org.ofbiz.base.util.UtilGenerics;
import org.ofbiz.base.util.UtilMisc;
import org.ofbiz.base.util.UtilValidate;
import org.ofbiz.base.util.cache.UtilCache;
/** Expands String values that contain Unified Expression Language (JSR 245)
* syntax. This class also supports the execution of bsh scripts by using the
* 'bsh:' prefix, and Groovy scripts by using the 'groovy:' prefix.
* Further it is possible to control the output by specifying the suffix
* '?currency(XXX)' to format the output according to the supplied locale
* and specified (XXX) currency.<p>This class extends the UEL by allowing
* nested expressions.</p>
*/
@SourceMonitored
@SuppressWarnings("serial")
public abstract class FlexibleStringExpander implements Serializable, IsEmpty {
public static final String module = FlexibleStringExpander.class.getName();
public static final String openBracket = "${";
public static final String closeBracket = "}";
protected static final UtilCache<Key, FlexibleStringExpander> exprCache = UtilCache.createUtilCache("flexibleStringExpander.ExpressionCache");
protected static final FlexibleStringExpander nullExpr = new ConstSimpleElem(new char[0]);
/**
* Returns <code>true</code> if <code>fse</code> contains a <code>String</code> constant.
* @param fse The <code>FlexibleStringExpander</code> to test
* @return <code>true</code> if <code>fse</code> contains a <code>String</code> constant
*/
public static boolean containsConstant(FlexibleStringExpander fse) {
if (fse instanceof ConstSimpleElem || fse instanceof ConstOffsetElem) {
return true;
}
if (fse instanceof Elements) {
Elements fseElements = (Elements) fse;
for (FlexibleStringExpander childElement : fseElements.childElems) {
if (containsConstant(childElement)) {
return true;
}
}
}
return false;
}
/**
* Returns <code>true</code> if <code>fse</code> contains an expression.
* @param fse The <code>FlexibleStringExpander</code> to test
* @return <code>true</code> if <code>fse</code> contains an expression
*/
public static boolean containsExpression(FlexibleStringExpander fse) {
return !(fse instanceof ConstSimpleElem);
}
/**
* Returns <code>true</code> if <code>fse</code> contains a script.
* @param fse The <code>FlexibleStringExpander</code> to test
* @return <code>true</code> if <code>fse</code> contains a script
*/
public static boolean containsScript(FlexibleStringExpander fse) {
if (fse instanceof ScriptElem) {
return true;
}
if (fse instanceof Elements) {
Elements fseElements = (Elements) fse;
for (FlexibleStringExpander childElement : fseElements.childElems) {
if (containsScript(childElement)) {
return true;
}
}
}
return false;
}
/** Evaluate an expression and return the result as a <code>String</code>.
* Null expressions return <code>null</code>.
* A null <code>context</code> argument will return the original expression.
* <p>Note that the behavior of this method is not the same as using
* <code>FlexibleStringExpander.getInstance(expression).expandString(context)</code>
* because it returns <code>null</code> when given a null <code>expression</code>
* argument, and
* <code>FlexibleStringExpander.getInstance(expression).expandString(context)</code>
* returns an empty <code>String</code>.</p>
*
* @param expression The original expression
* @param context The evaluation context
* @return The original expression's evaluation result as a <code>String</code>
*/
public static String expandString(String expression, Map<String, ? extends Object> context) {
return expandString(expression, context, null, null);
}
/** Evaluate an expression and return the result as a <code>String</code>.
* Null expressions return <code>null</code>.
* A null <code>context</code> argument will return the original expression.
* <p>Note that the behavior of this method is not the same as using
* <code>FlexibleStringExpander.getInstance(expression).expandString(context, locale)</code>
* because it returns <code>null</code> when given a null <code>expression</code>
* argument, and
* <code>FlexibleStringExpander.getInstance(expression).expandString(context, locale)</code>
* returns an empty <code>String</code>.</p>
*
* @param expression The original expression
* @param context The evaluation context
* @param locale The locale to be used for localization
* @return The original expression's evaluation result as a <code>String</code>
*/
public static String expandString(String expression, Map<String, ? extends Object> context, Locale locale) {
return expandString(expression, context, null, locale);
}
/** Evaluate an expression and return the result as a <code>String</code>.
* Null expressions return <code>null</code>.
* A null <code>context</code> argument will return the original expression.
* <p>Note that the behavior of this method is not the same as using
* <code>FlexibleStringExpander.getInstance(expression).expandString(context, timeZone locale)</code>
* because it returns <code>null</code> when given a null <code>expression</code>
* argument, and
* <code>FlexibleStringExpander.getInstance(expression).expandString(context, timeZone, locale)</code>
* returns an empty <code>String</code>.</p>
*
* @param expression The original expression
* @param context The evaluation context
* @param timeZone The time zone to be used for localization
* @param locale The locale to be used for localization
* @return The original expression's evaluation result as a <code>String</code>
*/
public static String expandString(String expression, Map<String, ? extends Object> context, TimeZone timeZone, Locale locale) {
if (expression == null) {
return "";
}
if (context == null || !expression.contains(openBracket)) {
return expression;
}
FlexibleStringExpander fse = FlexibleStringExpander.getInstance(expression);
return fse.expandString(context, timeZone, locale);
}
/** Returns a <code>FlexibleStringExpander</code> object. <p>A null or
* empty argument will return a <code>FlexibleStringExpander</code>
* object that represents an empty expression. That object is a shared
* singleton, so there is no memory or performance penalty in using it.</p>
* <p>If the method is passed a <code>String</code> argument that doesn't
* contain an expression, the <code>FlexibleStringExpander</code> object
* that is returned does not perform any evaluations on the original
* <code>String</code> - any methods that return a <code>String</code>
* will return the original <code>String</code>. The object returned by
* this method is very compact - taking less memory than the original
* <code>String</code>.</p>
*
* @param expression The original expression
* @return A <code>FlexibleStringExpander</code> instance
*/
public static FlexibleStringExpander getInstance(String expression) {
return getInstance(expression, true);
}
/* Returns a <code>FlexibleStringExpander</code> object. <p>A null or
* empty argument will return a <code>FlexibleStringExpander</code>
* object that represents an empty expression. That object is a shared
* singleton, so there is no memory or performance penalty in using it.</p>
* <p>If the method is passed a <code>String</code> argument that doesn't
* contain an expression, the <code>FlexibleStringExpander</code> object
* that is returned does not perform any evaluations on the original
* <code>String</code> - any methods that return a <code>String</code>
* will return the original <code>String</code>. The object returned by
* this method is very compact - taking less memory than the original
* <code>String</code>.</p>
*
* @param expression The original expression
* @param useCache whether to store things into a global cache
* @return A <code>FlexibleStringExpander</code> instance
*/
public static FlexibleStringExpander getInstance(String expression, boolean useCache) {
if (UtilValidate.isEmpty(expression)) {
return nullExpr;
}
return getInstance(expression, expression.toCharArray(), 0, expression.length(), useCache);
}
private static FlexibleStringExpander getInstance(String expression, char[] chars, int offset, int length, boolean useCache) {
if (length == 0) {
return nullExpr;
}
if (!useCache) {
return parse(chars, offset, length);
}
// Remove the next nine lines to cache all expressions
if (!expression.contains(openBracket)) {
if (chars.length == length) {
return new ConstSimpleElem(chars);
} else {
return new ConstOffsetElem(chars, offset, length);
}
}
Key key = chars.length == length ? new SimpleKey(chars) : new OffsetKey(chars, offset, length);
FlexibleStringExpander fse = exprCache.get(key);
if (fse == null) {
exprCache.put(key, parse(chars, offset, length));
fse = exprCache.get(key);
}
return fse;
}
private static abstract class Key {
@Override
public final boolean equals(Object o) {
// No class test here, nor null, as this class is only used
// internally
return toString().equals(o.toString());
}
@Override
public final int hashCode() {
return toString().hashCode();
}
}
private static final class SimpleKey extends Key {
private final char[] chars;
protected SimpleKey(char[] chars) {
this.chars = chars;
}
@Override
public String toString() {
return new String(chars);
}
}
private static final class OffsetKey extends Key {
private final char[] chars;
private final int offset;
private final int length;
protected OffsetKey(char[] chars, int offset, int length) {
this.chars = chars;
this.offset = offset;
this.length = length;
}
@Override
public String toString() {
return new String(chars, offset, length);
}
}
private static FlexibleStringExpander parse(char[] chars, int offset, int length) {
FlexibleStringExpander[] strElems = getStrElems(chars, offset, length);
if (strElems.length == 1) {
return strElems[0];
} else {
return new Elements(chars, offset, length, strElems);
}
}
protected static FlexibleStringExpander[] getStrElems(char[] chars, int offset, int length) {
String expression = new String(chars, 0, length + offset);
int start = expression.indexOf(openBracket, offset);
if (start == -1) {
return new FlexibleStringExpander[] { new ConstOffsetElem(chars, offset, length) };
}
int origLen = length;
ArrayList<FlexibleStringExpander> strElems = new ArrayList<FlexibleStringExpander>();
int currentInd = offset;
int end = -1;
while (start != -1) {
end = expression.indexOf(closeBracket, start);
if (end == -1) {
Debug.logWarning("Found a ${ without a closing } (curly-brace) in the String: " + expression, module);
break;
}
// Check for escaped expression
boolean escapedExpression = (start - 1 >= 0 && expression.charAt(start - 1) == '\\');
if (start > currentInd) {
// append everything from the current index to the start of the expression
strElems.add(new ConstOffsetElem(chars, currentInd, (escapedExpression ? start -1 : start) - currentInd));
}
if (expression.indexOf("bsh:", start + 2) == start + 2 && !escapedExpression) {
// checks to see if this starts with a "bsh:", if so treat the rest of the expression as a bsh scriptlet
strElems.add(new ScriptElem(chars, start, Math.min(end + 1, start + length) - start, start + 6, end - start - 6));
} else if (expression.indexOf("groovy:", start + 2) == start + 2 && !escapedExpression) {
// checks to see if this starts with a "groovy:", if so treat the rest of the expression as a groovy scriptlet
strElems.add(new ScriptElem(chars, start, Math.min(end + 1, start + length) - start, start + 9, end - start - 9));
} else {
// Scan for matching closing bracket
int ptr = expression.indexOf(openBracket, start + 2);
while (ptr != -1 && end != -1 && ptr < end) {
end = expression.indexOf(closeBracket, end + 1);
ptr = expression.indexOf(openBracket, ptr + 2);
}
if (end == -1) {
end = origLen;
}
// Evaluation sequence is important - do not change it
if (escapedExpression) {
strElems.add(new ConstOffsetElem(chars, start, end + 1 - start));
} else {
String subExpression = expression.substring(start + 2, end);
int currencyPos = subExpression.indexOf("?currency(");
int closeParen = currencyPos > 0 ? subExpression.indexOf(")", currencyPos + 10) : -1;
if (closeParen != -1) {
strElems.add(new CurrElem(chars, start, Math.min(end + 1, start + length) - start, start + 2, end - start - 1));
} else if (subExpression.contains(openBracket)) {
strElems.add(new NestedVarElem(chars, start, Math.min(end + 1, start + length) - start, start + 2, Math.min(end - 2, start + length) - start));
} else {
strElems.add(new VarElem(chars, start, Math.min(end + 1, start + length) - start, start + 2, Math.min(end - 2, start + length) - start));
}
}
}
// reset the current index to after the expression, and the start to the beginning of the next expression
currentInd = end + 1;
if (currentInd > origLen + offset) {
currentInd = origLen + offset;
}
start = expression.indexOf(openBracket, currentInd);
}
// append the rest of the original string, ie after the last expression
if (currentInd < origLen + offset) {
strElems.add(new ConstOffsetElem(chars, currentInd, offset + length - currentInd));
}
return strElems.toArray(new FlexibleStringExpander[strElems.size()]);
}
// Note: a character array is used instead of a String to keep the memory footprint small.
protected final char[] chars;
protected int hint = 20;
protected FlexibleStringExpander(char[] chars) {
this.chars = chars;
}
protected abstract Object get(Map<String, ? extends Object> context, TimeZone timeZone, Locale locale);
private static Locale getLocale(Locale locale, Map<String, ? extends Object> context) {
if (locale == null) {
locale = (Locale) context.get("locale");
if (locale == null && context.containsKey("autoUserLogin")) {
Map<String, Object> autoUserLogin = UtilGenerics.cast(context.get("autoUserLogin"));
locale = UtilMisc.ensureLocale(autoUserLogin.get("lastLocale"));
}
if (locale == null) {
locale = Locale.getDefault();
}
}
return locale;
}
private static TimeZone getTimeZone(TimeZone timeZone, Map<String, ? extends Object> context) {
if (timeZone == null) {
timeZone = (TimeZone) context.get("timeZone");
if (timeZone == null && context.containsKey("autoUserLogin")) {
Map<String, String> autoUserLogin = UtilGenerics.cast(context.get("autoUserLogin"));
timeZone = UtilDateTime.toTimeZone(autoUserLogin.get("lastTimeZone"));
}
if (timeZone == null) {
timeZone = TimeZone.getDefault();
}
}
return timeZone;
}
/** Evaluate this object's expression and return the result as a <code>String</code>.
* Null or empty expressions return an empty <code>String</code>.
* A <code>null context</code> argument will return the original expression.
*
* @param context The evaluation context
* @return This object's expression result as a <code>String</code>
*/
public String expandString(Map<String, ? extends Object> context) {
return this.expandString(context, null, null);
}
/** Evaluate this object's expression and return the result as a <code>String</code>.
* Null or empty expressions return an empty <code>String</code>.
* A <code>null context</code> argument will return the original expression.
*
* @param context The evaluation context
* @param locale The locale to be used for localization
* @return This object's expression result as a <code>String</code>
*/
public String expandString(Map<String, ? extends Object> context, Locale locale) {
return this.expandString(context, null, locale);
}
/** Evaluate this object's expression and return the result as a <code>String</code>.
* Null or empty expressions return an empty <code>String</code>.
* A <code>null context</code> argument will return the original expression.
*
* @param context The evaluation context
* @param timeZone The time zone to be used for localization
* @param locale The locale to be used for localization
* @return This object's expression result as a <code>String</code>
*/
public String expandString(Map<String, ? extends Object> context, TimeZone timeZone, Locale locale) {
if (context == null) {
return this.toString();
}
timeZone = getTimeZone(timeZone, context);
locale = getLocale(locale, context);
Object obj = get(context, timeZone, locale);
StringBuilder buffer = new StringBuilder(this.hint);
try {
if (obj != null) {
if (obj instanceof String) {
buffer.append(obj);
} else {
buffer.append(ObjectType.simpleTypeConvert(obj, "String", null, timeZone, locale, true));
}
}
} catch (Exception e) {
buffer.append(obj);
}
if (buffer.length() > this.hint) {
this.hint = buffer.length();
}
return buffer.toString();
}
/** Evaluate this object's expression and return the result as an <code>Object</code>.
* Null or empty expressions return an empty <code>String</code>.
* A <code>null context</code> argument will return the original expression.
*
* @param context The evaluation context
* @return This object's expression result as a <code>String</code>
*/
public Object expand(Map<String, ? extends Object> context) {
return this.expand(context, null, null);
}
/** Evaluate this object's expression and return the result as an <code>Object</code>.
* Null or empty expressions return an empty <code>String</code>.
* A <code>null context</code> argument will return the original expression.
*
* @param context The evaluation context
* @param locale The locale to be used for localization
* @return This object's expression result as a <code>String</code>
*/
public Object expand(Map<String, ? extends Object> context, Locale locale) {
return this.expand(context, null, locale);
}
/** Evaluate this object's expression and return the result as an <code>Object</code>.
* Null or empty expressions return an empty <code>String</code>.
* A <code>null context</code> argument will return the original expression.
*
* @param context The evaluation context
* @param timeZone The time zone to be used for localization
* @param locale The locale to be used for localization
* @return This object's expression result as a <code>String</code>
*/
public Object expand(Map<String, ? extends Object> context, TimeZone timeZone, Locale locale) {
if (context == null) {
return null;
}
return get(context, getTimeZone(timeZone, context), getLocale(locale, context));
}
/** Returns a copy of the original expression.
*
* @return The original expression
*/
public abstract String getOriginal();
/** Returns <code>true</code> if the original expression is empty
* or <code>null</code>.
*
* @return <code>true</code> if the original expression is empty
* or <code>null</code>
*/
public abstract boolean isEmpty();
/** Returns a copy of the original expression.
*
* @return The original expression
*/
@Override
public String toString() {
return this.getOriginal();
}
protected static abstract class ArrayOffsetString extends FlexibleStringExpander {
protected final int offset;
protected final int length;
protected ArrayOffsetString(char[] chars, int offset, int length) {
super(chars);
this.offset = offset;
this.length = length;
}
@Override
public boolean isEmpty() {
// This is always false; the complex child classes can't be
// empty, as they contain at least ${; constant elements
// with a length of 0 will never be created.
return false;
}
@Override
public String getOriginal() {
return new String(this.chars, this.offset, this.length);
}
}
/** An object that represents a <code>String</code> constant portion of an expression. */
protected static class ConstSimpleElem extends FlexibleStringExpander {
protected ConstSimpleElem(char[] chars) {
super(chars);
}
@Override
public boolean isEmpty() {
return this.chars.length == 0;
}
@Override
public String getOriginal() {
return new String(this.chars);
}
@Override
public String expandString(Map<String, ? extends Object> context, TimeZone timeZone, Locale locale) {
return getOriginal();
}
@Override
protected Object get(Map<String, ? extends Object> context, TimeZone timeZone, Locale locale) {
return isEmpty() ? null : getOriginal();
}
}
/** An object that represents a <code>String</code> constant portion of an expression. */
protected static class ConstOffsetElem extends ArrayOffsetString {
protected ConstOffsetElem(char[] chars, int offset, int length) {
super(chars, offset, length);
}
@Override
protected Object get(Map<String, ? extends Object> context, TimeZone timeZone, Locale locale) {
return getOriginal();
}
@Override
public String expandString(Map<String, ? extends Object> context, TimeZone timeZone, Locale locale) {
return new String(this.chars, this.offset, this.length);
}
}
/** An object that represents a currency portion of an expression. */
protected static class CurrElem extends ArrayOffsetString {
protected final char[] valueStr;
protected final FlexibleStringExpander codeExpr;
protected CurrElem(char[] chars, int offset, int length, int parseStart, int parseLength) {
super(chars, offset, length);
String parse = new String(chars, parseStart, parseLength);
int currencyPos = parse.indexOf("?currency(");
int closeParen = parse.indexOf(")", currencyPos + 10);
this.codeExpr = FlexibleStringExpander.getInstance(parse, chars, parseStart + currencyPos + 10, closeParen - currencyPos - 10, true);
this.valueStr = openBracket.concat(parse.substring(0, currencyPos)).concat(closeBracket).toCharArray();
}
@Override
protected Object get(Map<String, ? extends Object> context, TimeZone timeZone, Locale locale) {
try {
Object obj = UelUtil.evaluate(context, new String(this.valueStr));
if (obj != null) {
String currencyCode = this.codeExpr.expandString(context, timeZone, locale);
return UtilFormatOut.formatCurrency(new BigDecimal(obj.toString()), currencyCode, locale);
}
} catch (PropertyNotFoundException e) {
if (Debug.verboseOn()) {
Debug.logVerbose("Error evaluating expression: " + e, module);
}
} catch (Exception e) {
Debug.logError("Error evaluating expression: " + e, module);
}
return null;
}
}
/** A container object that contains expression fragments. */
protected static class Elements extends ArrayOffsetString {
protected final FlexibleStringExpander[] childElems;
protected Elements(char[] chars, int offset, int length, FlexibleStringExpander[] childElems) {
super(chars, offset, length);
this.childElems = childElems;
}
@Override
protected Object get(Map<String, ? extends Object> context, TimeZone timeZone, Locale locale) {
StringBuilder buffer = new StringBuilder();
for (FlexibleStringExpander child : this.childElems) {
buffer.append(child.expandString(context, timeZone, locale));
}
return buffer.toString();
}
}
/** An object that represents a <code>${[groovy|bsh]:}</code> expression. */
protected static class ScriptElem extends ArrayOffsetString {
private final String language;
private final int parseStart;
private final int parseLength;
private final String script;
protected final Class<?> parsedScript;
protected ScriptElem(char[] chars, int offset, int length, int parseStart, int parseLength) {
super(chars, offset, length);
this.language = new String(this.chars, offset + 2, parseStart - offset - 3);
this.parseStart = parseStart;
this.parseLength = parseLength;
this.script = new String(this.chars, this.parseStart, this.parseLength);
this.parsedScript = ScriptUtil.parseScript(this.language, this.script);
}
@Override
protected Object get(Map<String, ? extends Object> context, TimeZone timeZone, Locale locale) {
try {
Map <String, Object> contextCopy = new HashMap<String, Object>(context);
Object obj = ScriptUtil.evaluate(this.language, this.script, this.parsedScript, contextCopy);
if (obj != null) {
return obj;
} else {
if (Debug.verboseOn()) {
Debug.logVerbose("Scriptlet evaluated to null [" + this + "].", module);
}
}
} catch (Exception e) {
Debug.logWarning(e, "Error evaluating scriptlet [" + this + "]; error was: " + e, module);
}
return null;
}
}
/** An object that represents a nested expression. */
protected static class NestedVarElem extends ArrayOffsetString {
protected final FlexibleStringExpander[] childElems;
protected NestedVarElem(char[] chars, int offset, int length, int parseStart, int parseLength) {
super(chars, offset, length);
this.childElems = getStrElems(chars, parseStart, parseLength);
if (length > this.hint) {
this.hint = length;
}
}
@Override
protected Object get(Map<String, ? extends Object> context, TimeZone timeZone, Locale locale) {
StringBuilder expr = new StringBuilder(this.hint);
for (FlexibleStringExpander child : this.childElems) {
expr.append(child.expandString(context, timeZone, locale));
}
if (expr.length() == 0) {
return "";
}
try {
return UelUtil.evaluate(context, openBracket.concat(expr.toString()).concat(closeBracket));
} catch (PropertyNotFoundException e) {
if (Debug.verboseOn()) {
Debug.logVerbose("Error evaluating expression: " + e, module);
}
} catch (Exception e) {
Debug.logError("Error evaluating expression: " + e, module);
}
return "";
}
}
/** An object that represents a simple, non-nested expression. */
protected static class VarElem extends ArrayOffsetString {
protected final char[] bracketedOriginal;
protected VarElem(char[] chars, int offset, int length, int parseStart, int parseLength) {
super(chars, offset, length);
this.bracketedOriginal = openBracket.concat(UelUtil.prepareExpression(new String(chars, parseStart, parseLength))).concat(closeBracket).toCharArray();
}
@Override
protected Object get(Map<String, ? extends Object> context, TimeZone timeZone, Locale locale) {
Object obj = null;
try {
obj = UelUtil.evaluate(context, new String(this.bracketedOriginal));
} catch (PropertyNotFoundException e) {
if (Debug.verboseOn()) {
Debug.logVerbose("Error evaluating expression " + this + ": " + e, module);
}
} catch (Exception e) {
Debug.logError("Error evaluating expression " + this + ": " + e, module);
}
return obj;
}
}
}