1 package org.codehaus.plexus.interpolation;
2
3 /*
4 * Copyright 2001-2008 Codehaus Foundation.
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19 import java.util.ArrayList;
20 import java.util.HashMap;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.WeakHashMap;
24 import java.util.regex.Matcher;
25 import java.util.regex.Pattern;
26
27 import org.codehaus.plexus.interpolation.util.StringUtils;
28
29 /**
30 * Expansion of the original RegexBasedInterpolator, found in plexus-utils, this
31 * interpolator provides options for setting custom prefix/suffix regex parts,
32 * and includes a {@link RecursionInterceptor} parameter in its interpolate(..)
33 * call, to allow the detection of cyclical expression references.
34 *
35 */
36 public class RegexBasedInterpolator implements Interpolator {
37
38 private String startRegex;
39
40 private String endRegex;
41
42 private Map existingAnswers = new HashMap();
43
44 private List<ValueSource> valueSources = new ArrayList<ValueSource>();
45
46 private List<InterpolationPostProcessor> postProcessors = new ArrayList<InterpolationPostProcessor>();
47
48 private boolean reusePatterns = false;
49
50 private boolean cacheAnswers = false;
51
52 public static final String DEFAULT_REGEXP = "\\$\\{(.+?)\\}";
53
54 /**
55 * the key is the regex the value is the Pattern
56 * At the class construction time the Map will contains the default Pattern
57 */
58 private Map<String, Pattern> compiledPatterns = new WeakHashMap<String, Pattern>();
59
60 /**
61 * Setup a basic interpolator.
62 * <p><b>NOTE:</b> You will have to call</p>
63 * {@link RegexBasedInterpolator#addValueSource(ValueSource)} at least once
64 * if you use this constructor!
65 */
66 public RegexBasedInterpolator() {
67 compiledPatterns.put(DEFAULT_REGEXP, Pattern.compile(DEFAULT_REGEXP));
68 }
69
70 /**
71 *
72 * @param reusePatterns already compiled patterns will be reused
73 */
74 public RegexBasedInterpolator(boolean reusePatterns) {
75 this();
76 this.reusePatterns = reusePatterns;
77 }
78
79 /**
80 * Setup an interpolator with no value sources, and the specified regex pattern
81 * prefix and suffix in place of the default one.
82 * <p><b>NOTE:</b> You will have to call
83 * {@link RegexBasedInterpolator#addValueSource(ValueSource)} at least once
84 * if you use this constructor!</p>
85 *
86 * @param startRegex start of the regular expression to use
87 * @param endRegex end of the regular expression to use
88 */
89 public RegexBasedInterpolator(String startRegex, String endRegex) {
90 this();
91 this.startRegex = startRegex;
92 this.endRegex = endRegex;
93 }
94
95 /**
96 * Setup a basic interpolator with the specified list of value sources.
97 *
98 * @param valueSources The list of value sources to use
99 */
100 public RegexBasedInterpolator(List valueSources) {
101 this();
102 this.valueSources.addAll(valueSources);
103 }
104
105 /**
106 * Setup an interpolator with the specified value sources, and the specified
107 * regex pattern prefix and suffix in place of the default one.
108 *
109 * @param startRegex start of the regular expression to use
110 * @param endRegex end of the regular expression to use
111 * @param valueSources The list of value sources to use
112 */
113 public RegexBasedInterpolator(String startRegex, String endRegex, List valueSources) {
114 this();
115 this.startRegex = startRegex;
116 this.endRegex = endRegex;
117 this.valueSources.addAll(valueSources);
118 }
119
120 /**
121 * {@inheritDoc}
122 */
123 public void addValueSource(ValueSource valueSource) {
124 valueSources.add(valueSource);
125 }
126
127 /**
128 * {@inheritDoc}
129 */
130 public void removeValuesSource(ValueSource valueSource) {
131 valueSources.remove(valueSource);
132 }
133
134 /**
135 * {@inheritDoc}
136 */
137 public void addPostProcessor(InterpolationPostProcessor postProcessor) {
138 postProcessors.add(postProcessor);
139 }
140
141 /**
142 * {@inheritDoc}
143 */
144 public void removePostProcessor(InterpolationPostProcessor postProcessor) {
145 postProcessors.remove(postProcessor);
146 }
147
148 /**
149 * Attempt to resolve all expressions in the given input string, using the
150 * given pattern to first trim an optional prefix from each expression. The
151 * supplied recursion interceptor will provide protection from expression
152 * cycles, ensuring that the input can be resolved or an exception is
153 * thrown.
154 *
155 * @param input The input string to interpolate
156 *
157 * @param thisPrefixPattern An optional pattern that should be trimmed from
158 * the start of any expressions found in the input.
159 *
160 * @param recursionInterceptor Used to protect the interpolation process
161 * from expression cycles, and throw an
162 * exception if one is detected.
163 */
164 public String interpolate(String input, String thisPrefixPattern, RecursionInterceptor recursionInterceptor)
165 throws InterpolationException {
166 if (input == null) {
167 // return empty String to prevent NPE too
168 return "";
169 }
170 if (recursionInterceptor == null) {
171 recursionInterceptor = new SimpleRecursionInterceptor();
172 }
173
174 if (thisPrefixPattern != null && thisPrefixPattern.length() == 0) {
175 thisPrefixPattern = null;
176 }
177
178 int realExprGroup = 2;
179 Pattern expressionPattern;
180 final String expressionDelimiterStart;
181 final String expressionDelimiterEnd;
182 if (startRegex != null || endRegex != null) {
183 if (thisPrefixPattern == null) {
184 expressionPattern = getPattern(startRegex + endRegex);
185 realExprGroup = 1;
186 } else {
187 expressionPattern = getPattern(startRegex + thisPrefixPattern + endRegex);
188 }
189 expressionDelimiterStart = startRegex;
190 expressionDelimiterEnd = endRegex;
191
192 } else {
193 expressionDelimiterStart = "${";
194 expressionDelimiterEnd = "}";
195 if (thisPrefixPattern != null) {
196 expressionPattern = getPattern("\\$\\{(" + thisPrefixPattern + ")?(.+?)\\}");
197 } else {
198 expressionPattern = getPattern(DEFAULT_REGEXP);
199 realExprGroup = 1;
200 }
201 }
202 try {
203 return interpolate(
204 input,
205 recursionInterceptor,
206 expressionPattern,
207 expressionDelimiterStart,
208 expressionDelimiterEnd,
209 realExprGroup);
210 } finally {
211 if (!cacheAnswers) {
212 clearAnswers();
213 }
214 }
215 }
216
217 private Pattern getPattern(String regExp) {
218 if (!reusePatterns) {
219 return Pattern.compile(regExp);
220 }
221
222 Pattern pattern;
223 synchronized (this) {
224 pattern = compiledPatterns.get(regExp);
225
226 if (pattern != null) {
227 return pattern;
228 }
229
230 pattern = Pattern.compile(regExp);
231 compiledPatterns.put(regExp, pattern);
232 }
233
234 return pattern;
235 }
236
237 /**
238 * Entry point for recursive resolution of an expression and all of its
239 * nested expressions.
240 *
241 * @todo Ensure unresolvable expressions don't trigger infinite recursion.
242 */
243 private String interpolate(
244 String input,
245 RecursionInterceptor recursionInterceptor,
246 Pattern expressionPattern,
247 String expressionDelimiterStart,
248 String expressionDelimiterEnd,
249 int realExprGroup)
250 throws InterpolationException {
251 if (input == null) {
252 // return empty String to prevent NPE too
253 return "";
254 }
255 String result = input;
256
257 Matcher matcher = expressionPattern.matcher(result);
258
259 while (matcher.find()) {
260 String wholeExpr = matcher.group(0);
261 String realExpr = matcher.group(realExprGroup);
262
263 if (realExpr.startsWith(".")) {
264 realExpr = realExpr.substring(1);
265 }
266
267 if (recursionInterceptor.hasRecursiveExpression(realExpr)) {
268 throw new InterpolationCycleException(recursionInterceptor, realExpr, wholeExpr);
269 }
270
271 recursionInterceptor.expressionResolutionStarted(realExpr);
272 try {
273 Object value = existingAnswers.get(realExpr);
274 for (ValueSource vs : valueSources) {
275 if (value != null) break;
276
277 value = vs.getValue(realExpr, expressionDelimiterStart, expressionDelimiterEnd);
278 }
279
280 if (value != null) {
281 value = interpolate(
282 String.valueOf(value),
283 recursionInterceptor,
284 expressionPattern,
285 expressionDelimiterStart,
286 expressionDelimiterEnd,
287 realExprGroup);
288
289 if (postProcessors != null && !postProcessors.isEmpty()) {
290 for (InterpolationPostProcessor postProcessor : postProcessors) {
291 Object newVal = postProcessor.execute(realExpr, value);
292 if (newVal != null) {
293 value = newVal;
294 break;
295 }
296 }
297 }
298
299 // could use:
300 // result = matcher.replaceFirst( stringValue );
301 // but this could result in multiple lookups of stringValue, and replaceAll is not correct behaviour
302 result = StringUtils.replace(result, wholeExpr, String.valueOf(value));
303
304 if (cacheAnswers) {
305 existingAnswers.put(realExpr, value);
306 }
307
308 matcher.reset(result);
309 }
310 } finally {
311 recursionInterceptor.expressionResolutionFinished(realExpr);
312 }
313 }
314
315 return result;
316 }
317
318 /**
319 * Return any feedback messages and errors that were generated - but
320 * suppressed - during the interpolation process. Since unresolvable
321 * expressions will be left in the source string as-is, this feedback is
322 * optional, and will only be useful for debugging interpolation problems.
323 *
324 * @return a {@link List} that may be interspersed with {@link String} and
325 * {@link Throwable} instances.
326 */
327 public List getFeedback() {
328 List messages = new ArrayList();
329 for (Object valueSource : valueSources) {
330 ValueSource vs = (ValueSource) valueSource;
331 List feedback = vs.getFeedback();
332 if (feedback != null && !feedback.isEmpty()) {
333 messages.addAll(feedback);
334 }
335 }
336
337 return messages;
338 }
339
340 /**
341 * Clear the feedback messages from previous interpolate(..) calls.
342 */
343 public void clearFeedback() {
344 for (Object valueSource : valueSources) {
345 ValueSource vs = (ValueSource) valueSource;
346 vs.clearFeedback();
347 }
348 }
349
350 /**
351 * See {@link RegexBasedInterpolator#interpolate(String, String, RecursionInterceptor)}.
352 * <p>
353 * This method triggers the use of a {@link SimpleRecursionInterceptor}
354 * instance for protection against expression cycles.</p>
355 *
356 * @param input The input string to interpolate
357 *
358 * @param thisPrefixPattern An optional pattern that should be trimmed from
359 * the start of any expressions found in the input.
360 */
361 public String interpolate(String input, String thisPrefixPattern) throws InterpolationException {
362 return interpolate(input, thisPrefixPattern, null);
363 }
364
365 /**
366 * See {@link RegexBasedInterpolator#interpolate(String, String, RecursionInterceptor)}.
367 * <p>
368 * This method triggers the use of a {@link SimpleRecursionInterceptor}
369 * instance for protection against expression cycles. It also leaves empty the
370 * expression prefix which would otherwise be trimmed from expressions. The
371 * result is that any detected expression will be resolved as-is.</p>
372 *
373 * @param input The input string to interpolate
374 */
375 public String interpolate(String input) throws InterpolationException {
376 return interpolate(input, null, null);
377 }
378
379 /**
380 * See {@link RegexBasedInterpolator#interpolate(String, String, RecursionInterceptor)}.
381 * <p>
382 * This method leaves empty the expression prefix which would otherwise be
383 * trimmed from expressions. The result is that any detected expression will
384 * be resolved as-is.</p>
385 *
386 * @param input The input string to interpolate
387 *
388 * @param recursionInterceptor Used to protect the interpolation process
389 * from expression cycles, and throw an
390 * exception if one is detected.
391 */
392 public String interpolate(String input, RecursionInterceptor recursionInterceptor) throws InterpolationException {
393 return interpolate(input, null, recursionInterceptor);
394 }
395
396 public boolean isReusePatterns() {
397 return reusePatterns;
398 }
399
400 public void setReusePatterns(boolean reusePatterns) {
401 this.reusePatterns = reusePatterns;
402 }
403
404 public boolean isCacheAnswers() {
405 return cacheAnswers;
406 }
407
408 public void setCacheAnswers(boolean cacheAnswers) {
409 this.cacheAnswers = cacheAnswers;
410 }
411
412 public void clearAnswers() {
413 existingAnswers.clear();
414 }
415 }