001: /*
002: * Copyright 2006-2007 The Scriptella Project Team.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package scriptella.expression;
017:
018: import scriptella.spi.ParametersCallback;
019: import scriptella.spi.support.MapParametersCallback;
020: import scriptella.util.IOUtils;
021:
022: import java.io.IOException;
023: import java.io.Reader;
024: import java.io.Writer;
025: import java.util.Map;
026: import java.util.regex.Matcher;
027: import java.util.regex.Pattern;
028:
029: /**
030: * Substitutes properties(or expressions) in strings.
031: * <p>$ symbol indicate property or expression to evaluate and substitute.
032: * <p>The following properties/expression syntax is used:
033: * <h3>Property reference</h3>
034: * References named property.
035: * <br>Examples:
036: * <pre><code>
037: * $foo
038: * </code></pre>
039: * <h3>Expression</h3>.
040: * Expression is wrapped by braces and evaluated by {@link Expression} engine.
041: * Examples:
042: * <pre><code>
043: * ${name+' '+surname} etc.
044: * </code></pre>
045: * </ul>
046: * <p>This class is not thread safe
047: *
048: * @author Fyodor Kupolov
049: * @version 1.0
050: */
051: public class PropertiesSubstitutor {
052: /**
053: * Simple property patterns, e.g. $property
054: */
055: public static final Pattern PROP_PTR = Pattern
056: .compile("([a-zA-Z_0-9\\.]+)");
057:
058: /**
059: * Expression pattern, e.g. ${property} etc.
060: */
061: public static final Pattern EXPR_PTR = Pattern
062: .compile("\\{([^\\}]+)\\}");
063:
064: final Matcher m1 = PROP_PTR.matcher("");
065: final Matcher m2 = EXPR_PTR.matcher("");
066:
067: /**
068: * Creates a properties substitutor.
069: * <p>This constructor is used for performance critical places where multiple instantiation
070: * via {@link #PropertiesSubstitutor(scriptella.spi.ParametersCallback)} is expensive.
071: * <p><b>Note:</b> {@link #setParameters(scriptella.spi.ParametersCallback)} must be called before
072: * {@link #substitute(String)}.
073: */
074: public PropertiesSubstitutor() {
075: }
076:
077: /**
078: * Creates a properties substitutor.
079: *
080: * @param parameters parameters callback to use for substitution.
081: */
082: public PropertiesSubstitutor(ParametersCallback parameters) {
083: this .parameters = parameters;
084: }
085:
086: /**
087: * Creates a properties substitutor based on specified properties map.
088: *
089: * @param map parameters to substitute.
090: */
091: public PropertiesSubstitutor(Map<String, ?> map) {
092: this (new MapParametersCallback(map));
093: }
094:
095: private ParametersCallback parameters;
096:
097: /**
098: * Substitutes properties/expressions in s and returns the result string.
099: * <p>If result of evaluation is null or the property being substitued doesn't have value in callback - the whole
100: * expressions is copied into result string as is.
101: *
102: * @param s string to substitute. Null strings allowed.
103: * @return substituted string.
104: */
105: public String substitute(final String s) {
106: if (parameters == null) {
107: throw new IllegalStateException(
108: "setParameters must be called before calling substitute");
109: }
110:
111: int i = firstCandidate(s); //Remember the first index of $
112: if (i < 0) { //skip strings without $ char, or when the $ is the last char
113: return s;
114: }
115: final int len = s.length() - 1; //Last character is not checked - optimization
116: StringBuilder res = null;
117: int lastPos = 0;
118: m1.reset(s);
119: m2.reset(s);
120:
121: for (; i >= 0 && i < len; i = s.indexOf('$', i + 1)) {
122: //Start of expression
123: Matcher m;
124: if (m1.find(i + 1) && m1.start() == i + 1) {
125: m = m1;
126: } else if (m2.find(i + 1) && m2.start() == i + 1) {
127: m = m2;
128: } else { //not an expression
129: m = null;
130: }
131: if (m != null) {
132: final String name = m.group(1);
133: String v;
134:
135: if (m == m1) {
136: v = toString(parameters.getParameter(name));
137: } else {
138: v = toString(Expression.compile(name).evaluate(
139: parameters));
140: }
141:
142: if (v != null) {
143: if (res == null) {
144: res = new StringBuilder(s.length());
145: }
146: if (i > lastPos) { //if we have unflushed character
147: res.append(s.substring(lastPos, i));
148: }
149: lastPos = m.end();
150: res.append(v);
151: }
152:
153: }
154: }
155: if (res == null) {
156: return s;
157: }
158: if (lastPos <= len) {
159: res.append(s.substring(lastPos, s.length()));
160: }
161: return res.toString();
162: }
163:
164: /**
165: * Copies content from reader to writer and expands properties.
166: *
167: * @param reader reader to process.
168: * @param writer writer to output substituted content to.
169: * @throws IOException if I/O error occurs.
170: */
171: public void substitute(final Reader reader, final Writer writer)
172: throws IOException {
173: //Current implementation is too simple,
174: // we need to provide a better implementation for stream based content.
175: writer.write(substitute(IOUtils.toString(reader)));
176: }
177:
178: /**
179: * Reads content from reader and expands properties.
180: * <p><b>Note:</b> For performance reasons use
181: * {@link #substitute(java.io.Reader,java.io.Writer)} if possible.
182: *
183: * @param reader reader to process.
184: * @return reader's content with properties expanded.
185: * @throws IOException if I/O error occurs.
186: * @see #substitute(java.io.Reader,java.io.Writer)
187: */
188: public String substitute(final Reader reader) throws IOException {
189: //Current implementation is too simple,
190: // we need to provide a better implementation for stream based content.
191: return substitute(IOUtils.toString(reader));
192: }
193:
194: /**
195: * @return parameter callback used for substitution.
196: */
197: public ParametersCallback getParameters() {
198: return parameters;
199: }
200:
201: /**
202: * Sets parameters callback used for substitution.
203: *
204: * @param parameters not null parameters callback.
205: */
206: public void setParameters(ParametersCallback parameters) {
207: this .parameters = parameters;
208: }
209:
210: /**
211: * Converts specified object to string.
212: * <p>Subclasses may provide custom conversion strategy here.
213: *
214: * @param o object to convert to String.
215: * @return string representation of object.
216: */
217: protected String toString(final Object o) {
218: return o == null ? null : o.toString();
219: }
220:
221: /**
222: * Tests if the given string contains properties/expressions.
223: * @param string string to check.
224: * @return true if a given string contains properties/expressions.
225: */
226: public static boolean hasProperties(String string) {
227: return firstCandidate(string) >= 0;
228: }
229:
230: static int firstCandidate(String string) {
231: if (string == null) {
232: return -1;
233: }
234: int n = string.length();
235: if (n < 2) {
236: return -1;
237: }
238: return string.indexOf('$');
239: }
240:
241: }
|