View Javadoc
1   package org.codehaus.plexus.util;
2   
3   /*
4    * Copyright The 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.io.FilterReader;
20  import java.io.IOException;
21  import java.io.PushbackReader;
22  import java.io.Reader;
23  import java.util.Collections;
24  import java.util.HashSet;
25  import java.util.Map;
26  import java.util.Set;
27  import java.util.TreeMap;
28  
29  import org.codehaus.plexus.util.reflection.Reflector;
30  import org.codehaus.plexus.util.reflection.ReflectorException;
31  
32  /**
33   * A FilterReader which interpolates keyword values into a character stream. Keywords are recognized when enclosed
34   * between starting and ending delimiter strings. The keywords themselves, and their values, are fetched from a Map
35   * supplied to the constructor.
36   * <p>
37   * When a possible keyword token is recognized (by detecting the starting and ending token delimiters):
38   * </p>
39   * <ul>
40   * <li>if the enclosed string is found in the keyword Map, the delimiters and the keyword are effectively replaced by
41   * the keyword's value;</li>
42   * <li>if the enclosed string is found in the keyword Map, but its value has zero length, then the token (delimiters and
43   * keyword) is effectively removed from the character stream;</li>
44   * <li>if the enclosed string is <em>not</em> found in the keyword Map, then no substitution is made; the token text is
45   * passed through unaltered.</li>
46   * </ul>
47   *
48   * <p>A token in the incoming character stream may be <em>escaped</em> by prepending an "escape sequence" which is
49   * specified to the constructor. An escaped token is passed through as written, with the escape sequence removed. This
50   * allows things which would look like tokens to be read literally rather than interpolated.</p>
51   *
52   * @author jdcasey Created on Feb 3, 2005
53   * @see InterpolationFilterReader
54   */
55  public class LineOrientedInterpolatingReader extends FilterReader {
56      public static final String DEFAULT_START_DELIM = "${";
57  
58      public static final String DEFAULT_END_DELIM = "}";
59  
60      public static final String DEFAULT_ESCAPE_SEQ = "\\";
61  
62      private static final char CARRIAGE_RETURN_CHAR = '\r';
63  
64      private static final char NEWLINE_CHAR = '\n';
65  
66      private final PushbackReader pushbackReader;
67  
68      private final Map<String, Object> context;
69  
70      private final String startDelim;
71  
72      private final String endDelim;
73  
74      private final String escapeSeq;
75  
76      private final int minExpressionSize;
77  
78      private final Reflector reflector;
79  
80      private int lineIdx = -1;
81  
82      private String line;
83  
84      /**
85       * Construct an interpolating Reader, specifying token delimiters and the escape sequence.
86       *
87       * @param reader the Reader to be filtered.
88       * @param context keyword/value pairs for interpolation.
89       * @param startDelim character sequence which (possibly) begins a token.
90       * @param endDelim character sequence which ends a token.
91       * @param escapeSeq escape sequence
92       */
93      public LineOrientedInterpolatingReader(
94              Reader reader, Map<String, ?> context, String startDelim, String endDelim, String escapeSeq) {
95          super(reader);
96  
97          this.startDelim = startDelim;
98  
99          this.endDelim = endDelim;
100 
101         this.escapeSeq = escapeSeq;
102 
103         // Expressions have to be at least this size...
104         this.minExpressionSize = startDelim.length() + endDelim.length() + 1;
105 
106         this.context = Collections.unmodifiableMap(context);
107 
108         this.reflector = new Reflector();
109 
110         if (reader instanceof PushbackReader) {
111             this.pushbackReader = (PushbackReader) reader;
112         } else {
113             this.pushbackReader = new PushbackReader(reader, 1);
114         }
115     }
116 
117     /**
118      * Filters a Reader using the default escape sequence "\".
119      *
120      * @param reader the Reader to be filtered.
121      * @param context keyword/value pairs for interpolation.
122      * @param startDelim the character sequence which (possibly) begins a token.
123      * @param endDelim the character sequence which ends a token.
124      */
125     public LineOrientedInterpolatingReader(Reader reader, Map<String, ?> context, String startDelim, String endDelim) {
126         this(reader, context, startDelim, endDelim, DEFAULT_ESCAPE_SEQ);
127     }
128 
129     /**
130      * Filters a Reader using the default escape sequence "\" and token delimiters "${", "}".
131      *
132      * @param reader the Reader to be filtered.
133      * @param context keyword/value pairs for interpolation.
134      */
135     public LineOrientedInterpolatingReader(Reader reader, Map<String, ?> context) {
136         this(reader, context, DEFAULT_START_DELIM, DEFAULT_END_DELIM, DEFAULT_ESCAPE_SEQ);
137     }
138 
139     @Override
140     public int read() throws IOException {
141         if (line == null || lineIdx >= line.length()) {
142             readAndInterpolateLine();
143         }
144 
145         int next = -1;
146 
147         if (line != null && lineIdx < line.length()) {
148             next = line.charAt(lineIdx++);
149         }
150 
151         return next;
152     }
153 
154     @Override
155     public int read(char[] cbuf, int off, int len) throws IOException {
156         int fillCount = 0;
157 
158         for (int i = off; i < off + len; i++) {
159             int next = read();
160             if (next > -1) {
161                 cbuf[i] = (char) next;
162             } else {
163                 break;
164             }
165 
166             fillCount++;
167         }
168 
169         if (fillCount == 0) {
170             fillCount = -1;
171         }
172 
173         return fillCount;
174     }
175 
176     @Override
177     public long skip(long n) throws IOException {
178         long skipCount = 0;
179 
180         for (long i = 0; i < n; i++) {
181             int next = read();
182 
183             if (next < 0) {
184                 break;
185             }
186 
187             skipCount++;
188         }
189 
190         return skipCount;
191     }
192 
193     private void readAndInterpolateLine() throws IOException {
194         String rawLine = readLine();
195 
196         if (rawLine != null) {
197             Set<String> expressions = parseForExpressions(rawLine);
198 
199             Map<String, Object> evaluatedExpressions = evaluateExpressions(expressions);
200 
201             String interpolated = replaceWithInterpolatedValues(rawLine, evaluatedExpressions);
202 
203             if (interpolated != null && interpolated.length() > 0) {
204                 line = interpolated;
205                 lineIdx = 0;
206             }
207         } else {
208             line = null;
209             lineIdx = -1;
210         }
211     }
212 
213     /*
214      * Read one line from the wrapped Reader. A line is a sequence of characters ending in CRLF, CR, or LF. The
215      * terminating character(s) will be included in the returned line.
216      */
217     private String readLine() throws IOException {
218         StringBuilder lineBuffer = new StringBuilder(40); // half of the "normal" line maxsize
219         int next;
220 
221         boolean lastWasCR = false;
222         while ((next = pushbackReader.read()) > -1) {
223             char c = (char) next;
224 
225             if (c == CARRIAGE_RETURN_CHAR) {
226                 lastWasCR = true;
227                 lineBuffer.append(c);
228             } else if (c == NEWLINE_CHAR) {
229                 lineBuffer.append(c);
230                 break; // end of line.
231             } else if (lastWasCR) {
232                 pushbackReader.unread(c);
233                 break;
234             } else {
235                 lineBuffer.append(c);
236             }
237         }
238 
239         if (lineBuffer.length() < 1) {
240             return null;
241         } else {
242             return lineBuffer.toString();
243         }
244     }
245 
246     private String replaceWithInterpolatedValues(String rawLine, Map<String, Object> evaluatedExpressions) {
247         String result = rawLine;
248 
249         for (Object o : evaluatedExpressions.entrySet()) {
250             Map.Entry entry = (Map.Entry) o;
251 
252             String expression = (String) entry.getKey();
253 
254             String value = String.valueOf(entry.getValue());
255 
256             result = findAndReplaceUnlessEscaped(result, expression, value);
257         }
258 
259         return result;
260     }
261 
262     private Map<String, Object> evaluateExpressions(Set<String> expressions) {
263         Map<String, Object> evaluated = new TreeMap<String, Object>();
264 
265         for (Object expression : expressions) {
266             String rawExpression = (String) expression;
267 
268             String realExpression =
269                     rawExpression.substring(startDelim.length(), rawExpression.length() - endDelim.length());
270 
271             String[] parts = realExpression.split("\\.");
272             if (parts.length > 0) {
273                 Object value = context.get(parts[0]);
274 
275                 if (value != null) {
276                     for (int i = 1; i < parts.length; i++) {
277                         try {
278                             value = reflector.getObjectProperty(value, parts[i]);
279 
280                             if (value == null) {
281                                 break;
282                             }
283                         } catch (ReflectorException e) {
284                             // TODO: Fix this! It should report, but not interrupt.
285                             e.printStackTrace();
286 
287                             break;
288                         }
289                     }
290 
291                     evaluated.put(rawExpression, value);
292                 }
293             }
294         }
295 
296         return evaluated;
297     }
298 
299     private Set<String> parseForExpressions(String rawLine) {
300         Set<String> expressions = new HashSet<String>();
301 
302         if (rawLine != null) {
303             int placeholder = -1;
304 
305             do {
306                 // find the beginning of the next expression.
307                 int start = findDelimiter(rawLine, startDelim, placeholder);
308 
309                 // if we can't find a start-delimiter, then there is no valid expression. Ignore everything else.
310                 if (start < 0) {
311                     // no expression found.
312                     break;
313                 }
314 
315                 // find the end of the next expression.
316                 int end = findDelimiter(rawLine, endDelim, start + 1);
317 
318                 // if we can't find an end-delimiter, then this is not a valid expression. Ignore it.
319                 if (end < 0) {
320                     // no VALID expression found.
321                     break;
322                 }
323 
324                 // if we reach this point, we have a valid start and end position, which
325                 // means we have a valid expression. So, we add it to the set of
326                 // expressions in need of evaluation.
327                 expressions.add(rawLine.substring(start, end + endDelim.length()));
328 
329                 // increment the placeholder so we can look beyond this expression.
330                 placeholder = end + 1;
331             } while (placeholder < rawLine.length() - minExpressionSize);
332         }
333 
334         return expressions;
335     }
336 
337     private int findDelimiter(String rawLine, String delimiter, int lastPos) {
338         int placeholder = lastPos;
339 
340         int position;
341         do {
342             position = rawLine.indexOf(delimiter, placeholder);
343 
344             if (position < 0) {
345                 break;
346             } else {
347                 int escEndIdx = rawLine.indexOf(escapeSeq, placeholder) + escapeSeq.length();
348 
349                 if (escEndIdx > escapeSeq.length() - 1 && escEndIdx == position) {
350                     placeholder = position + 1;
351                     position = -1;
352                 }
353             }
354 
355         } while (position < 0 && placeholder < rawLine.length() - endDelim.length());
356         // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
357         // use length() - endDelim.length() b/c otherwise there is nothing left to search.
358 
359         return position;
360     }
361 
362     private String findAndReplaceUnlessEscaped(String rawLine, String search, String replace) {
363         StringBuilder lineBuffer = new StringBuilder((int) (rawLine.length() * 1.5));
364 
365         int lastReplacement = -1;
366 
367         do {
368             int nextReplacement = rawLine.indexOf(search, lastReplacement + 1);
369             if (nextReplacement > -1) {
370                 if (lastReplacement < 0) {
371                     lastReplacement = 0;
372                 }
373 
374                 lineBuffer.append(rawLine, lastReplacement, nextReplacement);
375 
376                 int escIdx = rawLine.indexOf(escapeSeq, lastReplacement + 1);
377                 if (escIdx > -1 && escIdx + escapeSeq.length() == nextReplacement) {
378                     lineBuffer.setLength(lineBuffer.length() - escapeSeq.length());
379                     lineBuffer.append(search);
380                 } else {
381                     lineBuffer.append(replace);
382                 }
383 
384                 lastReplacement = nextReplacement + search.length();
385             } else {
386                 break;
387             }
388         } while (lastReplacement > -1);
389 
390         if (lastReplacement < rawLine.length()) {
391             lineBuffer.append(rawLine, lastReplacement, rawLine.length());
392         }
393 
394         return lineBuffer.toString();
395     }
396 }