/* * Copyright 2006-2012 The Scriptella Project Team. * * 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 scriptella.expression; import scriptella.spi.ParametersCallback; import scriptella.spi.support.MapParametersCallback; import scriptella.util.IOUtils; import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Substitutes properties(or expressions) in strings. * <p>$ symbol indicate property or expression to evaluate and substitute. * <p>The following properties/expression syntax is used: * <h3>Property reference</h3> * References named property. * <br>Examples: * <pre><code> * $foo * </code></pre> * <h3>Expression</h3>. * Expression is wrapped by braces and evaluated by {@link Expression} engine. * Examples: * <pre><code> * ${name+' '+surname} etc. * </code></pre> * </ul> * <p>This class is not thread safe * * @author Fyodor Kupolov * @version 1.0 */ public class PropertiesSubstitutor { /** * Simple property patterns, e.g. $property */ public static final Pattern PROP_PTR = Pattern.compile("([a-zA-Z_0-9\\.]+)"); /** * Expression pattern, e.g. ${property} etc. */ public static final Pattern EXPR_PTR = Pattern.compile("\\{([^\\}]+)\\}"); final Matcher m1 = PROP_PTR.matcher(""); final Matcher m2 = EXPR_PTR.matcher(""); private ParametersCallback parameters; private String nullString; /** * Creates a properties substitutor. * <p>This constructor is used for performance critical places where multiple instantiation * via {@link #PropertiesSubstitutor(scriptella.spi.ParametersCallback)} is expensive. * <p><b>Note:</b> {@link #setParameters(scriptella.spi.ParametersCallback)} must be called before * {@link #substitute(String)}. */ public PropertiesSubstitutor() { } /** * Creates a properties substitutor. * * @param parameters parameters callback to use for substitution. */ public PropertiesSubstitutor(ParametersCallback parameters) { this.parameters = parameters; } /** * Creates a properties substitutor based on specified properties map. * * @param map parameters to substitute. */ public PropertiesSubstitutor(Map<String, ?> map) { this(new MapParametersCallback(map)); } /** * Substitutes properties/expressions in s and returns the result string. * <p>If result of evaluation is null or the property being substitued doesn't have value in callback - the whole * expressions is copied into result string as is. * * @param s string to substitute. Null strings allowed. * @return substituted string. */ public String substitute(final String s) { if (parameters == null) { throw new IllegalStateException("setParameters must be called before calling substitute"); } int i = firstCandidate(s); //Remember the first index of $ if (i < 0) { //skip strings without $ char, or when the $ is the last char return s; } final int len = s.length() - 1; //Last character is not checked - optimization StringBuilder res = null; int lastPos = 0; m1.reset(s); m2.reset(s); for (; i >= 0 && i < len; i = s.indexOf('$', i + 1)) { //Start of expression Matcher m; if (m1.find(i + 1) && m1.start() == i + 1) { m = m1; } else if (m2.find(i + 1) && m2.start() == i + 1) { m = m2; } else { //not an expression m = null; } if (m != null) { final String name = m.group(1); String v; if (m == m1) { v = toString(parameters.getParameter(name)); } else { v = toString(Expression.compile(name).evaluate(parameters)); } if (v != null) { if (res == null) { res = new StringBuilder(s.length()); } if (i > lastPos) { //if we have unflushed character res.append(s.substring(lastPos, i)); } lastPos = m.end(); res.append(v); } } } if (res == null) { return s; } if (lastPos <= len) { res.append(s.substring(lastPos, s.length())); } return res.toString(); } /** * Copies content from reader to writer and expands properties. * * @param reader reader to process. * @param writer writer to output substituted content to. * @throws IOException if I/O error occurs. */ public void substitute(final Reader reader, final Writer writer) throws IOException { //Current implementation is too simple, // we need to provide a better implementation for stream based content. writer.write(substitute(IOUtils.toString(reader))); } /** * Reads content from reader and expands properties. * <p><b>Note:</b> For performance reasons use * {@link #substitute(java.io.Reader,java.io.Writer)} if possible. * * @param reader reader to process. * @return reader's content with properties expanded. * @throws IOException if I/O error occurs. * @see #substitute(java.io.Reader,java.io.Writer) */ public String substitute(final Reader reader) throws IOException { //Current implementation is too simple, // we need to provide a better implementation for stream based content. return substitute(IOUtils.toString(reader)); } /** * @return parameter callback used for substitution. */ public ParametersCallback getParameters() { return parameters; } /** * Sets parameters callback used for substitution. * * @param parameters not null parameters callback. */ public void setParameters(ParametersCallback parameters) { this.parameters = parameters; } /** * Returns string literal representing null value. * <p>Used when converting objects {@link #toString(Object)}. * * @return string representing null value. */ public String getNullString() { return nullString; } /** * Sets string literal representing null. * <p>Used when converting objects {@link #toString(Object)}. * * @param nullString string literal representing null */ public void setNullString(String nullString) { this.nullString = nullString; } /** * Converts specified object to string. * <p>{@link #getNullString()} represents null values. * <p>Subclasses may provide custom conversion strategy here. * * @param o object to convert to String. * @return string representation of object. */ protected String toString(final Object o) { return o == null ? nullString : o.toString(); } /** * Tests if the given string contains properties/expressions. * * @param string string to check. * @return true if a given string contains properties/expressions. */ public static boolean hasProperties(String string) { return firstCandidate(string) >= 0; } static int firstCandidate(String string) { if (string == null) { return -1; } int n = string.length(); if (n < 2) { return -1; } return string.indexOf('$'); } }