001: /*******************************************************************************
002: * Licensed to the Apache Software Foundation (ASF) under one
003: * or more contributor license agreements. See the NOTICE file
004: * distributed with this work for additional information
005: * regarding copyright ownership. The ASF licenses this file
006: * to you under the Apache License, Version 2.0 (the
007: * "License"); you may not use this file except in compliance
008: * with the License. You may obtain a copy of the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing,
013: * software distributed under the License is distributed on an
014: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015: * KIND, either express or implied. See the License for the
016: * specific language governing permissions and limitations
017: * under the License.
018: *******************************************************************************/package org.ofbiz.base.util.string;
019:
020: import java.io.Serializable;
021: import java.util.Iterator;
022: import java.util.LinkedList;
023: import java.util.List;
024: import java.util.Locale;
025: import java.util.Map;
026:
027: import org.ofbiz.base.util.BshUtil;
028: import org.ofbiz.base.util.Debug;
029: import org.ofbiz.base.util.collections.FlexibleMapAccessor;
030: import org.ofbiz.base.util.UtilFormatOut;
031: import org.ofbiz.base.util.UtilMisc;
032:
033: import bsh.EvalError;
034:
035: /**
036: * Expands string values with in a Map context supporting the ${} syntax for
037: * variable placeholders and the "." (dot) and "[]" (square-brace) syntax
038: * elements for accessing Map entries and List elements in the context.
039: * It Also supports the execution of bsh files by using the 'bsh:' prefix.
040: * Further it is possible to control the output by specifying the suffix
041: * '?currency(XXX)' to format the output according the current locale
042: * and specified (XXX) currency
043: *
044: */
045: public class FlexibleStringExpander implements Serializable {
046:
047: public static final String module = FlexibleStringExpander.class
048: .getName();
049:
050: protected String original;
051: protected List stringElements = new LinkedList();
052: protected static boolean localizeCurrency = false;
053: protected static String currencyCode = null;
054:
055: public FlexibleStringExpander(String original) {
056: this .original = original;
057:
058: ParseElementHandler handler = new PreParseHandler(
059: stringElements);
060: parseString(original, handler);
061: }
062:
063: public boolean isEmpty() {
064: if (this .original == null || this .original.length() == 0) {
065: return true;
066: } else {
067: return false;
068: }
069: }
070:
071: public String getOriginal() {
072: return this .original;
073: }
074:
075: /**
076: * This expands the pre-parsed String given the context passed in. Note that
077: * pre-parsing can only parse the top-level place-holders and if there are
078: * nested expansions they will be done on the fly instead of pre-parsed because
079: * they are dependent on the context which isn't known until expansion time.
080: *
081: * @param context A context Map containing the variable values
082: * @return The original String expanded by replacing varaible place holders.
083: */
084: public String expandString(Map context) {
085: return this .expandString(context, null);
086: }
087:
088: /**
089: * This expands the pre-parsed String given the context passed in. Note that
090: * pre-parsing can only parse the top-level place-holders and if there are
091: * nested expansions they will be done on the fly instead of pre-parsed because
092: * they are dependent on the context which isn't known until expansion time.
093: *
094: * @param context A context Map containing the variable values
095: * @param locale the current set locale
096: * @return The original String expanded by replacing varaible place holders.
097: */
098: public String expandString(Map context, Locale locale) {
099: StringBuffer expanded = new StringBuffer();
100:
101: Iterator stringElementIter = stringElements.iterator();
102: while (stringElementIter.hasNext()) {
103: StringElement element = (StringElement) stringElementIter
104: .next();
105: element.appendElement(expanded, context, locale);
106: }
107:
108: //call back into this method with new String to take care of any/all nested expands
109: return expandString(expanded.toString(), context, locale);
110: }
111:
112: /**
113: * Does on-the-fly parsing and expansion of the original String using
114: * varaible values from the passed context. Variables are denoted with
115: * the "${}" syntax and the variable name inside the curly-braces can use
116: * the "." (dot) syntax to access sub-Map entries and the "[]" square-brace
117: * syntax to access List elements.
118: * It Also supports the execution of bsh files by using the 'bsh:' prefix.
119: * Further it is possible to control the output by specifying the suffix
120: * '?currency(XXX)' to format the output according the current locale
121: * and specified (XXX) currency
122: *
123: * @param original The original String that will be expanded
124: * @param context A context Map containing the variable values
125: * @return The original String expanded by replacing varaible place holders.
126: */
127: public static String expandString(String original, Map context) {
128: return expandString(original, context, null);
129: }
130:
131: /**
132: * Does on-the-fly parsing and expansion of the original String using
133: * varaible values from the passed context. Variables are denoted with
134: * the "${}" syntax and the variable name inside the curly-braces can use
135: * the "." (dot) syntax to access sub-Map entries and the "[]" square-brace
136: * syntax to access List elements.
137: * It Also supports the execution of bsh files by using the 'bsh:' prefix.
138: * Further it is possible to control the output by specifying the suffix
139: * '?currency(XXX)' to format the output according the current locale
140: * and specified (XXX) currency
141: *
142: * @param original The original String that will be expanded
143: * @param context A context Map containing the variable values
144: * @return The original String expanded by replacing varaible place holders.
145: */
146: public static String expandString(String original, Map context,
147: Locale locale) {
148: // if null or less than 3 return original; 3 chars because that is the minimum necessary for a ${}
149: if (original == null || original.length() < 3) {
150: return original;
151: }
152:
153: // start by checking to see if expansion is necessary for better performance
154: // this is also necessary for the nested stuff since this will be the stopping point for nested expansions
155: int start = original.indexOf("${");
156: if (start == -1) {
157: return original;
158: } else {
159: if (original.indexOf("}", start) == -1) {
160: //no ending for the start, so we also have a stop condition
161: Debug.logWarning(
162: "Found a ${ without a closing } (curly-brace) in the String: "
163: + original, module);
164: return original;
165: }
166: }
167:
168: if (locale == null && context.containsKey("locale")) {
169: locale = (Locale) context.get("locale");
170: }
171: if (locale == null && context.containsKey("autoUserLogin")) {
172: locale = UtilMisc.ensureLocale(((Map) context
173: .get("autoUserLogin")).get("lastLocale"));
174: }
175:
176: StringBuffer expanded = new StringBuffer();
177: // TODO: for performance to save object build up and tear down times we should use Javolution to make OnTheFlyHandler reusable and use a factory methods instead of constructor
178: ParseElementHandler handler = new OnTheFlyHandler(expanded,
179: context, locale);
180: parseString(original, handler);
181:
182: //call back into this method with new String to take care of any/all nested expands
183: return expandString(expanded.toString(), context, locale);
184: }
185:
186: public static void parseString(String original,
187: ParseElementHandler handler) {
188: if (original == null || original.length() == 0) {
189: return;
190: }
191:
192: int start = original.indexOf("${");
193: if (start == -1) {
194: handler.handleConstant(original, 0);
195: return;
196: }
197: int currentInd = 0;
198: int end = -1;
199: while (start != -1) {
200: end = original.indexOf("}", start);
201: if (end == -1) {
202: Debug.logWarning(
203: "Found a ${ without a closing } (curly-brace) in the String: "
204: + original, module);
205: break;
206: }
207:
208: // check to see if there is a nested ${}, ie something like ${foo.${bar}} or ${foo[$bar}]}
209: // since we are only handling one at a time, and then recusively looking for nested ones, just look backward from the } for another ${ and if found and is not the same start spot, update the start spot
210: int possibleNestedStart = original.lastIndexOf("${", end);
211: if (start != possibleNestedStart) {
212: // found a nested one, could print something here, but just do the simple thing...
213: start = possibleNestedStart;
214: }
215:
216: // append everything from the current index to the start of the var
217: handler.handleConstant(original, currentInd, start);
218:
219: // check to see if this starts with a "bsh:", if so treat the rest of the string as a bsh scriptlet
220: if (original.indexOf("bsh:", start + 2) == start + 2) {
221: // get the bsh scriptlet and append it
222: handler.handleBsh(original, start + 6, end);
223: } else {
224: // get the environment value and append it
225: handler.handleVariable(original, start + 2, end);
226: }
227:
228: // reset the current index to after the var, and the start to the beginning of the next var
229: currentInd = end + 1;
230: start = original.indexOf("${", currentInd);
231: }
232:
233: // append the rest of the original string, ie after the last variable
234: if (currentInd < original.length()) {
235: handler.handleConstant(original, currentInd);
236: }
237: }
238:
239: public static interface StringElement extends Serializable {
240: public void appendElement(StringBuffer buffer, Map context,
241: Locale locale);
242: }
243:
244: public static class ConstantElement implements StringElement {
245: protected String value;
246:
247: public ConstantElement(String value) {
248: this .value = value;
249: }
250:
251: public void appendElement(StringBuffer buffer, Map context,
252: Locale locale) {
253: buffer.append(this .value);
254: }
255: }
256:
257: public static class BshElement implements StringElement {
258: String scriptlet;
259:
260: public BshElement(String scriptlet) {
261: this .scriptlet = scriptlet;
262: }
263:
264: public void appendElement(StringBuffer buffer, Map context,
265: Locale locale) {
266: try {
267: Object scriptResult = BshUtil.eval(scriptlet, context);
268: if (scriptResult != null) {
269: buffer.append(scriptResult.toString());
270: } else {
271: Debug.logWarning("BSH scriplet evaluated to null ["
272: + scriptlet
273: + "], got no return so inserting nothing.",
274: module);
275: }
276: } catch (EvalError e) {
277: Debug.logWarning(e, "Error evaluating BSH scriplet ["
278: + scriptlet
279: + "], inserting nothing; error was: "
280: + e.toString(), module);
281: }
282: }
283: }
284:
285: public static class VariableElement implements StringElement {
286: protected FlexibleMapAccessor fma;
287:
288: public VariableElement(String valueName) {
289: this .fma = new FlexibleMapAccessor(valueName);
290: }
291:
292: public void appendElement(StringBuffer buffer, Map context,
293: Locale locale) {
294: Object retVal = fma.get(context, locale);
295: if (retVal != null) {
296: buffer.append(retVal.toString());
297: } else {
298: // otherwise do nothing
299: }
300: }
301: }
302:
303: public static interface ParseElementHandler extends Serializable {
304: public void handleConstant(String original, int start);
305:
306: public void handleConstant(String original, int start, int end);
307:
308: public void handleVariable(String original, int start, int end);
309:
310: public void handleBsh(String original, int start, int end);
311: }
312:
313: public static class PreParseHandler implements ParseElementHandler {
314: protected List stringElements;
315:
316: public PreParseHandler(List stringElements) {
317: this .stringElements = stringElements;
318: }
319:
320: public void handleConstant(String original, int start) {
321: stringElements.add(new ConstantElement(original
322: .substring(start)));
323: }
324:
325: public void handleConstant(String original, int start, int end) {
326: stringElements.add(new ConstantElement(original.substring(
327: start, end)));
328: }
329:
330: public void handleVariable(String original, int start, int end) {
331: stringElements.add(new VariableElement(original.substring(
332: start, end)));
333: }
334:
335: public void handleBsh(String original, int start, int end) {
336: stringElements.add(new BshElement(original.substring(start,
337: end)));
338: }
339: }
340:
341: public static class OnTheFlyHandler implements ParseElementHandler {
342: protected StringBuffer targetBuffer;
343: protected Map context;
344: protected Locale locale;
345:
346: public OnTheFlyHandler(StringBuffer targetBuffer, Map context,
347: Locale locale) {
348: this .targetBuffer = targetBuffer;
349: this .context = context;
350: this .locale = locale;
351: }
352:
353: public void handleConstant(String original, int start) {
354: targetBuffer.append(original.substring(start));
355: }
356:
357: public void handleConstant(String original, int start, int end) {
358: targetBuffer.append(original.substring(start, end));
359: }
360:
361: public void handleVariable(String original, int start, int end) {
362: //get the environment value and append it
363: String envName = original.substring(start, end);
364:
365: // see if ?currency(xxx) formatting is required
366: localizeCurrency = false;
367: int currencyPos = envName.indexOf("?currency(");
368: if (currencyPos != -1) {
369: int closeBracket = envName.indexOf(")",
370: currencyPos + 10);
371: currencyCode = envName.substring(currencyPos + 10,
372: closeBracket);
373: localizeCurrency = true;
374: envName = envName.substring(0, currencyPos);
375: }
376:
377: FlexibleMapAccessor fma = new FlexibleMapAccessor(envName);
378: Object envVal = fma.get(context, locale);
379: if (envVal != null) {
380: if (localizeCurrency) {
381: targetBuffer.append(UtilFormatOut.formatCurrency(
382: new Double(envVal.toString()),
383: currencyCode, locale));
384: } else {
385: targetBuffer.append(envVal.toString());
386: }
387: } else {
388: Debug.logWarning(
389: "Could not find value in environment for the name ["
390: + envName + "], inserting nothing.",
391: module);
392: }
393: }
394:
395: public void handleBsh(String original, int start, int end) {
396: //run the scriplet and append the result
397: String scriptlet = original.substring(start, end);
398: try {
399: Object scriptResult = BshUtil.eval(scriptlet, context);
400: if (scriptResult != null) {
401: targetBuffer.append(scriptResult.toString());
402: } else {
403: Debug.logWarning("BSH scriplet evaluated to null ["
404: + scriptlet
405: + "], got no return so inserting nothing.",
406: module);
407: }
408: } catch (EvalError e) {
409: Debug.logWarning(e, "Error evaluating BSH scriplet ["
410: + scriptlet
411: + "], inserting nothing; error was: "
412: + e.toString(), module);
413: }
414: }
415: }
416: }
|