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 org.codehaus.plexus.util.reflection.Reflector;
20  import org.codehaus.plexus.util.reflection.ReflectorException;
21  
22  import java.io.FilterReader;
23  import java.io.IOException;
24  import java.io.PushbackReader;
25  import java.io.Reader;
26  import java.util.Collections;
27  import java.util.HashSet;
28  import java.util.Map;
29  import java.util.Set;
30  import java.util.TreeMap;
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
56      extends FilterReader
57  {
58      public static final String DEFAULT_START_DELIM = "${";
59  
60      public static final String DEFAULT_END_DELIM = "}";
61  
62      public static final String DEFAULT_ESCAPE_SEQ = "\\";
63  
64      private static final char CARRIAGE_RETURN_CHAR = '\r';
65  
66      private static final char NEWLINE_CHAR = '\n';
67  
68      private final PushbackReader pushbackReader;
69  
70      private final Map<String, Object> context;
71  
72      private final String startDelim;
73  
74      private final String endDelim;
75  
76      private final String escapeSeq;
77  
78      private final int minExpressionSize;
79  
80      private final Reflector reflector;
81  
82      private int lineIdx = -1;
83  
84      private String line;
85  
86      /**
87       * Construct an interpolating Reader, specifying token delimiters and the escape sequence.
88       * 
89       * @param reader the Reader to be filtered.
90       * @param context keyword/value pairs for interpolation.
91       * @param startDelim character sequence which (possibly) begins a token.
92       * @param endDelim character sequence which ends a token.
93       * @param escapeSeq escape sequence
94       */
95      public LineOrientedInterpolatingReader( Reader reader, Map<String, ?> context, String startDelim, String endDelim,
96                                              String escapeSeq )
97      {
98          super( reader );
99  
100         this.startDelim = startDelim;
101 
102         this.endDelim = endDelim;
103 
104         this.escapeSeq = escapeSeq;
105 
106         // Expressions have to be at least this size...
107         this.minExpressionSize = startDelim.length() + endDelim.length() + 1;
108 
109         this.context = Collections.unmodifiableMap( context );
110 
111         this.reflector = new Reflector();
112 
113         if ( reader instanceof PushbackReader )
114         {
115             this.pushbackReader = (PushbackReader) reader;
116         }
117         else
118         {
119             this.pushbackReader = new PushbackReader( reader, 1 );
120         }
121     }
122 
123     /**
124      * Filters a Reader using the default escape sequence "\".
125      * 
126      * @param reader the Reader to be filtered.
127      * @param context keyword/value pairs for interpolation.
128      * @param startDelim the character sequence which (possibly) begins a token.
129      * @param endDelim the character sequence which ends a token.
130      */
131     public LineOrientedInterpolatingReader( Reader reader, Map<String, ?> context, String startDelim, String endDelim )
132     {
133         this( reader, context, startDelim, endDelim, DEFAULT_ESCAPE_SEQ );
134     }
135 
136     /**
137      * Filters a Reader using the default escape sequence "\" and token delimiters "${", "}".
138      * 
139      * @param reader the Reader to be filtered.
140      * @param context keyword/value pairs for interpolation.
141      */
142     public LineOrientedInterpolatingReader( Reader reader, Map<String, ?> context )
143     {
144         this( reader, context, DEFAULT_START_DELIM, DEFAULT_END_DELIM, DEFAULT_ESCAPE_SEQ );
145     }
146 
147     @Override
148     public int read()
149         throws IOException
150     {
151         if ( line == null || lineIdx >= line.length() )
152         {
153             readAndInterpolateLine();
154         }
155 
156         int next = -1;
157 
158         if ( line != null && lineIdx < line.length() )
159         {
160             next = line.charAt( lineIdx++ );
161         }
162 
163         return next;
164     }
165 
166     @Override
167     public int read( char[] cbuf, int off, int len )
168         throws IOException
169     {
170         int fillCount = 0;
171 
172         for ( int i = off; i < off + len; i++ )
173         {
174             int next = read();
175             if ( next > -1 )
176             {
177                 cbuf[i] = (char) next;
178             }
179             else
180             {
181                 break;
182             }
183 
184             fillCount++;
185         }
186 
187         if ( fillCount == 0 )
188         {
189             fillCount = -1;
190         }
191 
192         return fillCount;
193     }
194 
195     @Override
196     public long skip( long n )
197         throws IOException
198     {
199         long skipCount = 0;
200 
201         for ( long i = 0; i < n; i++ )
202         {
203             int next = read();
204 
205             if ( next < 0 )
206             {
207                 break;
208             }
209 
210             skipCount++;
211         }
212 
213         return skipCount;
214     }
215 
216     private void readAndInterpolateLine()
217         throws IOException
218     {
219         String rawLine = readLine();
220 
221         if ( rawLine != null )
222         {
223             Set<String> expressions = parseForExpressions( rawLine );
224 
225             Map<String, Object> evaluatedExpressions = evaluateExpressions( expressions );
226 
227             String interpolated = replaceWithInterpolatedValues( rawLine, evaluatedExpressions );
228 
229             if ( interpolated != null && interpolated.length() > 0 )
230             {
231                 line = interpolated;
232                 lineIdx = 0;
233             }
234         }
235         else
236         {
237             line = null;
238             lineIdx = -1;
239         }
240     }
241 
242     /*
243      * Read one line from the wrapped Reader. A line is a sequence of characters ending in CRLF, CR, or LF. The
244      * terminating character(s) will be included in the returned line.
245      */
246     private String readLine()
247         throws IOException
248     {
249         StringBuilder lineBuffer = new StringBuilder( 40 ); // half of the "normal" line maxsize
250         int next;
251 
252         boolean lastWasCR = false;
253         while ( ( next = pushbackReader.read() ) > -1 )
254         {
255             char c = (char) next;
256 
257             if ( c == CARRIAGE_RETURN_CHAR )
258             {
259                 lastWasCR = true;
260                 lineBuffer.append( c );
261             }
262             else if ( c == NEWLINE_CHAR )
263             {
264                 lineBuffer.append( c );
265                 break; // end of line.
266             }
267             else if ( lastWasCR )
268             {
269                 pushbackReader.unread( c );
270                 break;
271             }
272             else
273             {
274                 lineBuffer.append( c );
275             }
276         }
277 
278         if ( lineBuffer.length() < 1 )
279         {
280             return null;
281         }
282         else
283         {
284             return lineBuffer.toString();
285         }
286     }
287 
288     private String replaceWithInterpolatedValues( String rawLine, Map<String, Object> evaluatedExpressions )
289     {
290         String result = rawLine;
291 
292         for ( Object o : evaluatedExpressions.entrySet() )
293         {
294             Map.Entry entry = (Map.Entry) o;
295 
296             String expression = (String) entry.getKey();
297 
298             String value = String.valueOf( entry.getValue() );
299 
300             result = findAndReplaceUnlessEscaped( result, expression, value );
301         }
302 
303         return result;
304     }
305 
306     private Map<String, Object> evaluateExpressions( Set<String> expressions )
307     {
308         Map<String, Object> evaluated = new TreeMap<String, Object>();
309 
310         for ( Object expression : expressions )
311         {
312             String rawExpression = (String) expression;
313 
314             String realExpression =
315                 rawExpression.substring( startDelim.length(), rawExpression.length() - endDelim.length() );
316 
317             String[] parts = realExpression.split( "\\." );
318             if ( parts.length > 0 )
319             {
320                 Object value = context.get( parts[0] );
321 
322                 if ( value != null )
323                 {
324                     for ( int i = 1; i < parts.length; i++ )
325                     {
326                         try
327                         {
328                             value = reflector.getObjectProperty( value, parts[i] );
329 
330                             if ( value == null )
331                             {
332                                 break;
333                             }
334                         }
335                         catch ( ReflectorException e )
336                         {
337                             // TODO: Fix this! It should report, but not interrupt.
338                             e.printStackTrace();
339 
340                             break;
341                         }
342                     }
343 
344                     evaluated.put( rawExpression, value );
345                 }
346             }
347         }
348 
349         return evaluated;
350     }
351 
352     private Set<String> parseForExpressions( String rawLine )
353     {
354         Set<String> expressions = new HashSet<String>();
355 
356         if ( rawLine != null )
357         {
358             int placeholder = -1;
359 
360             do
361             {
362                 // find the beginning of the next expression.
363                 int start = findDelimiter( rawLine, startDelim, placeholder );
364 
365                 // if we can't find a start-delimiter, then there is no valid expression. Ignore everything else.
366                 if ( start < 0 )
367                 {
368                     // no expression found.
369                     break;
370                 }
371 
372                 // find the end of the next expression.
373                 int end = findDelimiter( rawLine, endDelim, start + 1 );
374 
375                 // if we can't find an end-delimiter, then this is not a valid expression. Ignore it.
376                 if ( end < 0 )
377                 {
378                     // no VALID expression found.
379                     break;
380                 }
381 
382                 // if we reach this point, we have a valid start and end position, which
383                 // means we have a valid expression. So, we add it to the set of
384                 // expressions in need of evaluation.
385                 expressions.add( rawLine.substring( start, end + endDelim.length() ) );
386 
387                 // increment the placeholder so we can look beyond this expression.
388                 placeholder = end + 1;
389             }
390             while ( placeholder < rawLine.length() - minExpressionSize );
391         }
392 
393         return expressions;
394     }
395 
396     private int findDelimiter( String rawLine, String delimiter, int lastPos )
397     {
398         int placeholder = lastPos;
399 
400         int position;
401         do
402         {
403             position = rawLine.indexOf( delimiter, placeholder );
404 
405             if ( position < 0 )
406             {
407                 break;
408             }
409             else
410             {
411                 int escEndIdx = rawLine.indexOf( escapeSeq, placeholder ) + escapeSeq.length();
412 
413                 if ( escEndIdx > escapeSeq.length() - 1 && escEndIdx == position )
414                 {
415                     placeholder = position + 1;
416                     position = -1;
417                 }
418             }
419 
420         }
421         while ( position < 0 && placeholder < rawLine.length() - endDelim.length() );
422         // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
423         // use length() - endDelim.length() b/c otherwise there is nothing left to search.
424 
425         return position;
426     }
427 
428     private String findAndReplaceUnlessEscaped( String rawLine, String search, String replace )
429     {
430         StringBuilder lineBuffer = new StringBuilder( (int) ( rawLine.length() * 1.5 ) );
431 
432         int lastReplacement = -1;
433 
434         do
435         {
436             int nextReplacement = rawLine.indexOf( search, lastReplacement + 1 );
437             if ( nextReplacement > -1 )
438             {
439                 if ( lastReplacement < 0 )
440                 {
441                     lastReplacement = 0;
442                 }
443 
444                 lineBuffer.append( rawLine, lastReplacement, nextReplacement );
445 
446                 int escIdx = rawLine.indexOf( escapeSeq, lastReplacement + 1 );
447                 if ( escIdx > -1 && escIdx + escapeSeq.length() == nextReplacement )
448                 {
449                     lineBuffer.setLength( lineBuffer.length() - escapeSeq.length() );
450                     lineBuffer.append( search );
451                 }
452                 else
453                 {
454                     lineBuffer.append( replace );
455                 }
456 
457                 lastReplacement = nextReplacement + search.length();
458             }
459             else
460             {
461                 break;
462             }
463         }
464         while ( lastReplacement > -1 );
465 
466         if ( lastReplacement < rawLine.length() )
467         {
468             lineBuffer.append( rawLine, lastReplacement, rawLine.length() );
469         }
470 
471         return lineBuffer.toString();
472     }
473 
474 }