Coverage Report - org.codehaus.plexus.util.LineOrientedInterpolatingReader
 
Classes in this File Line Coverage Branch Coverage Complexity
LineOrientedInterpolatingReader
84%
114/135
75%
59/78
4.154
 
 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  7
     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  7
         super( reader );
 101  
 
 102  7
         this.startDelim = startDelim;
 103  
 
 104  7
         this.endDelim = endDelim;
 105  
 
 106  7
         this.escapeSeq = escapeSeq;
 107  
 
 108  
         // Expressions have to be at least this size...
 109  7
         this.minExpressionSize = startDelim.length() + endDelim.length() + 1;
 110  
 
 111  7
         this.context = Collections.unmodifiableMap( context );
 112  
 
 113  7
         this.reflector = new Reflector();
 114  
 
 115  7
         if ( reader instanceof PushbackReader )
 116  
         {
 117  0
             this.pushbackReader = (PushbackReader) reader;
 118  
         }
 119  
         else
 120  
         {
 121  7
             this.pushbackReader = new PushbackReader( reader, 1 );
 122  
         }
 123  7
     }
 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  3
         this( reader, context, startDelim, endDelim, DEFAULT_ESCAPE_SEQ );
 136  3
     }
 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  4
         this( reader, context, DEFAULT_START_DELIM, DEFAULT_END_DELIM, DEFAULT_ESCAPE_SEQ );
 147  4
     }
 148  
 
 149  
     public int read()
 150  
         throws IOException
 151  
     {
 152  222
         if ( line == null || lineIdx >= line.length() )
 153  
         {
 154  21
             readAndInterpolateLine();
 155  
         }
 156  
 
 157  222
         int next = -1;
 158  
 
 159  222
         if ( line != null && lineIdx < line.length() )
 160  
         {
 161  208
             next = line.charAt( lineIdx++ );
 162  
         }
 163  
 
 164  222
         return next;
 165  
     }
 166  
 
 167  
     public int read( char[] cbuf, int off, int len )
 168  
         throws IOException
 169  
     {
 170  14
         int fillCount = 0;
 171  
 
 172  222
         for ( int i = off; i < off + len; i++ )
 173  
         {
 174  222
             int next = read();
 175  222
             if ( next > -1 )
 176  
             {
 177  208
                 cbuf[i] = (char) next;
 178  
             }
 179  
             else
 180  
             {
 181  
                 break;
 182  
             }
 183  
 
 184  208
             fillCount++;
 185  
         }
 186  
 
 187  14
         if ( fillCount == 0 )
 188  
         {
 189  7
             fillCount = -1;
 190  
         }
 191  
 
 192  14
         return fillCount;
 193  
     }
 194  
 
 195  
     public long skip( long n )
 196  
         throws IOException
 197  
     {
 198  0
         long skipCount = 0;
 199  
 
 200  0
         for ( long i = 0; i < n; i++ )
 201  
         {
 202  0
             int next = read();
 203  
 
 204  0
             if ( next < 0 )
 205  
             {
 206  0
                 break;
 207  
             }
 208  
 
 209  0
             skipCount++;
 210  
         }
 211  
 
 212  0
         return skipCount;
 213  
     }
 214  
 
 215  
     private void readAndInterpolateLine()
 216  
         throws IOException
 217  
     {
 218  21
         String rawLine = readLine();
 219  
 
 220  21
         if ( rawLine != null )
 221  
         {
 222  7
             Set<String> expressions = parseForExpressions( rawLine );
 223  
 
 224  7
             Map<String, Object> evaluatedExpressions = evaluateExpressions( expressions );
 225  
 
 226  7
             String interpolated = replaceWithInterpolatedValues( rawLine, evaluatedExpressions );
 227  
 
 228  7
             if ( interpolated != null && interpolated.length() > 0 )
 229  
             {
 230  7
                 line = interpolated;
 231  7
                 lineIdx = 0;
 232  
             }
 233  7
         }
 234  
         else
 235  
         {
 236  14
             line = null;
 237  14
             lineIdx = -1;
 238  
         }
 239  21
     }
 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  21
         StringBuilder lineBuffer = new StringBuilder( 40 ); // half of the "normal" line maxsize
 249  
         int next;
 250  
 
 251  21
         boolean lastWasCR = false;
 252  237
         while ( ( next = pushbackReader.read() ) > -1 )
 253  
         {
 254  216
             char c = (char) next;
 255  
 
 256  216
             if ( c == CARRIAGE_RETURN_CHAR )
 257  
             {
 258  0
                 lastWasCR = true;
 259  0
                 lineBuffer.append( c );
 260  
             }
 261  216
             else if ( c == NEWLINE_CHAR )
 262  
             {
 263  0
                 lineBuffer.append( c );
 264  0
                 break; // end of line.
 265  
             }
 266  216
             else if ( lastWasCR )
 267  
             {
 268  0
                 pushbackReader.unread( c );
 269  0
                 break;
 270  
             }
 271  
             else
 272  
             {
 273  216
                 lineBuffer.append( c );
 274  
             }
 275  216
         }
 276  
 
 277  21
         if ( lineBuffer.length() < 1 )
 278  
         {
 279  14
             return null;
 280  
         }
 281  
         else
 282  
         {
 283  7
             return lineBuffer.toString();
 284  
         }
 285  
     }
 286  
 
 287  
     private String replaceWithInterpolatedValues( String rawLine, Map<String, Object> evaluatedExpressions )
 288  
     {
 289  7
         String result = rawLine;
 290  
 
 291  7
         for ( Object o : evaluatedExpressions.entrySet() )
 292  
         {
 293  11
             Map.Entry entry = (Map.Entry) o;
 294  
 
 295  11
             String expression = (String) entry.getKey();
 296  
 
 297  11
             String value = String.valueOf( entry.getValue() );
 298  
 
 299  11
             result = findAndReplaceUnlessEscaped( result, expression, value );
 300  11
         }
 301  
 
 302  7
         return result;
 303  
     }
 304  
 
 305  
     private Map<String, Object> evaluateExpressions( Set<String> expressions )
 306  
     {
 307  7
         Map<String, Object> evaluated = new TreeMap<String, Object>();
 308  
 
 309  7
         for ( Object expression : expressions )
 310  
         {
 311  16
             String rawExpression = (String) expression;
 312  
 
 313  16
             String realExpression =
 314  
                 rawExpression.substring( startDelim.length(), rawExpression.length() - endDelim.length() );
 315  
 
 316  16
             String[] parts = realExpression.split( "\\." );
 317  16
             if ( parts.length > 0 )
 318  
             {
 319  16
                 Object value = context.get( parts[0] );
 320  
 
 321  16
                 if ( value != null )
 322  
                 {
 323  11
                     for ( int i = 1; i < parts.length; i++ )
 324  
                     {
 325  
                         try
 326  
                         {
 327  0
                             value = reflector.getObjectProperty( value, parts[i] );
 328  
 
 329  0
                             if ( value == null )
 330  
                             {
 331  0
                                 break;
 332  
                             }
 333  
                         }
 334  0
                         catch ( ReflectorException e )
 335  
                         {
 336  
                             // TODO: Fix this! It should report, but not interrupt.
 337  0
                             e.printStackTrace();
 338  
 
 339  0
                             break;
 340  0
                         }
 341  
                     }
 342  
 
 343  11
                     evaluated.put( rawExpression, value );
 344  
                 }
 345  
             }
 346  16
         }
 347  
 
 348  7
         return evaluated;
 349  
     }
 350  
 
 351  
     private Set<String> parseForExpressions( String rawLine )
 352  
     {
 353  7
         Set<String> expressions = new HashSet<String>();
 354  
 
 355  7
         if ( rawLine != null )
 356  
         {
 357  7
             int placeholder = -1;
 358  
 
 359  
             do
 360  
             {
 361  
                 // find the beginning of the next expression.
 362  18
                 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  18
                 if ( start < 0 )
 366  
                 {
 367  
                     // no expression found.
 368  1
                     break;
 369  
                 }
 370  
 
 371  
                 // find the end of the next expression.
 372  17
                 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  17
                 if ( end < 0 )
 376  
                 {
 377  
                     // no VALID expression found.
 378  1
                     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  16
                 expressions.add( rawLine.substring( start, end + endDelim.length() ) );
 385  
 
 386  
                 // increment the placeholder so we can look beyond this expression.
 387  16
                 placeholder = end + 1;
 388  
             }
 389  16
             while ( placeholder < rawLine.length() - minExpressionSize );
 390  
         }
 391  
 
 392  7
         return expressions;
 393  
     }
 394  
 
 395  
     private int findDelimiter( String rawLine, String delimiter, int lastPos )
 396  
     {
 397  35
         int placeholder = lastPos;
 398  
 
 399  
         int position;
 400  
         do
 401  
         {
 402  36
             position = rawLine.indexOf( delimiter, placeholder );
 403  
 
 404  36
             if ( position < 0 )
 405  
             {
 406  2
                 break;
 407  
             }
 408  
             else
 409  
             {
 410  34
                 int escEndIdx = rawLine.indexOf( escapeSeq, placeholder ) + escapeSeq.length();
 411  
 
 412  34
                 if ( escEndIdx > escapeSeq.length() - 1 && escEndIdx == position )
 413  
                 {
 414  1
                     placeholder = position + 1;
 415  1
                     position = -1;
 416  
                 }
 417  
             }
 418  
 
 419  
         }
 420  34
         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  35
         return position;
 425  
     }
 426  
 
 427  
     private String findAndReplaceUnlessEscaped( String rawLine, String search, String replace )
 428  
     {
 429  11
         StringBuilder lineBuffer = new StringBuilder( (int) ( rawLine.length() * 1.5 ) );
 430  
 
 431  11
         int lastReplacement = -1;
 432  
 
 433  
         do
 434  
         {
 435  23
             int nextReplacement = rawLine.indexOf( search, lastReplacement + 1 );
 436  23
             if ( nextReplacement > -1 )
 437  
             {
 438  12
                 if ( lastReplacement < 0 )
 439  
                 {
 440  11
                     lastReplacement = 0;
 441  
                 }
 442  
 
 443  12
                 lineBuffer.append( rawLine, lastReplacement, nextReplacement );
 444  
 
 445  12
                 int escIdx = rawLine.indexOf( escapeSeq, lastReplacement + 1 );
 446  12
                 if ( escIdx > -1 && escIdx + escapeSeq.length() == nextReplacement )
 447  
                 {
 448  1
                     lineBuffer.setLength( lineBuffer.length() - escapeSeq.length() );
 449  1
                     lineBuffer.append( search );
 450  
                 }
 451  
                 else
 452  
                 {
 453  11
                     lineBuffer.append( replace );
 454  
                 }
 455  
 
 456  12
                 lastReplacement = nextReplacement + search.length();
 457  
             }
 458  
             else
 459  
             {
 460  
                 break;
 461  
             }
 462  
         }
 463  12
         while ( lastReplacement > -1 );
 464  
 
 465  11
         if ( lastReplacement < rawLine.length() )
 466  
         {
 467  9
             lineBuffer.append( rawLine, lastReplacement, rawLine.length() );
 468  
         }
 469  
 
 470  11
         return lineBuffer.toString();
 471  
     }
 472  
 
 473  
 }