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