View Javadoc
1   /*
2    * Copyright 2018 Jeremy Long.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.owasp.maven.tools;
17  
18  import java.io.FilterReader;
19  import java.io.IOException;
20  import java.io.Reader;
21  import java.util.ArrayDeque;
22  import java.util.Deque;
23  
24  /**
25   * Reads a Velocity Template and filters leading whitespace and injects Velocity
26   * comments (##) to the end of each line.
27   *
28   * @author Jeremy Long
29   */
30  public class VelocityWhitespaceFilteringReader extends FilterReader {
31  
32      /**
33       * A cache of the previous three characters read.
34       */
35      private final ReaderCache cache = new ReaderCache();
36      /**
37       * A buffer for added content.
38       */
39      private final Deque<Character> buffer = new ArrayDeque<>();
40  
41      /**
42       * Tracks if a velocity comment is being read. Note, these are not filtered.
43       */
44      private boolean inComment = false;
45      /**
46       * Tracks if an uninterpreted section of a velocity template is being read.
47       */
48      private boolean inUninterpretted = false;
49      /**
50       * Tracks if we are starting a new line (we can strip leading spaces).
51       */
52      private boolean isNewLine = true;
53      /**
54       * Tracks if we are at the end of the file.
55       */
56      private boolean isEOF = false;
57  
58      /**
59       * Tracks whether or not a velocity variable is being output (e.g.
60       * $prop.something).
61       */
62      private boolean needsTrailingSpace = false;
63  
64      /**
65       * Creates a new Velocity whitespace filtering reader.
66       *
67       * @param reader the underlying reader
68       */
69      public VelocityWhitespaceFilteringReader(Reader reader) {
70          super(reader);
71      }
72  
73      /**
74       * {@inheritDoc}
75       */
76      @Override
77      public int read() throws IOException {
78          if (!buffer.isEmpty()) {
79              return (int) buffer.pop();
80          }
81          int c = super.read();
82          if (c == -1) {
83              return -1;
84          }
85          cache.push(c);
86  
87          if (inUninterpretted) {
88              if (cache.checkSequence(']', ']', '#')) {
89                  inUninterpretted = false;
90              }
91              return c;
92          } else if (inComment) {
93              if (cache.checkSequence('*', '#')) {
94                  inComment = false;
95              }
96              return c;
97          } else if (cache.checkSequence('#', '[', '[')) {
98              inUninterpretted = true;
99          } else if (cache.checkSequence('#', '*')) {
100             inComment = true;
101         }
102         if (!inComment && !inUninterpretted) {
103             if (isNewLine) {
104                 while (c == '\t' || c == ' ' || c == '\n' || c == '\r') {
105                     c = super.read();
106                     if (c == -1) {
107                         return -1;
108                     }
109                     cache.push(c);
110                 }
111                 isNewLine = false;
112             } else if (c == '\n' || c == '\r') {
113                 isNewLine = true;
114                 if (needsTrailingSpace && (cache.checkSequence(')', '\n') || cache.checkSequence(')', '\r')
115                         || cache.checkSequence(']', '\n') || cache.checkSequence(']', '\r'))) {
116                     needsTrailingSpace = false;
117                 }
118                 final char retVal;
119                 if (needsTrailingSpace) {
120                     buffer.add('#');
121                     retVal = ' ';
122                 } else {
123                     retVal = '#';
124                 }
125                 needsTrailingSpace = false;
126                 buffer.add('#');
127                 buffer.add((char) c);
128                 return retVal;
129             }
130             if (c == '$') {
131                 needsTrailingSpace = true;
132             } else if (needsTrailingSpace && checkIfNeedsTrailingSpace(c)) {
133                 needsTrailingSpace = false;
134             }
135         }
136         return (char) c;
137     }
138 
139     /**
140      * {@inheritDoc}
141      */
142     @Override
143     public int read(char cbuf[], int offset, int length) throws IOException {
144         if (isEOF) {
145             return -1;
146         }
147         int n;
148         for (n = 0; n < length; n++) {
149             final int c = read();
150             if (c == -1) {
151                 isEOF = true;
152                 return n;
153             }
154             cbuf[offset + n] = (char) c;
155         }
156         return n;
157     }
158 
159     /**
160      * Determines if the current velocity expression requires a trailing space
161      * before a single line comment is added (##).
162      *
163      * @param c the character to check
164      * @return <code>true</code> if a whitespace is needed; otherwise
165      * <code>false</code>
166      */
167     private boolean checkIfNeedsTrailingSpace(int c) {
168         return !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9'
169                 || c == '-' || c == '!' || c == '_' || c == '.'
170                 || c == '(' || c == ')' || c == '[' || c == ']');
171     }
172 
173     /**
174      * Small internal cache that is used to track the previous three characters
175      * read.
176      */
177     private static class ReaderCache {
178 
179         /**
180          * The cache.
181          */
182         private final int[] cache = new int[3];
183 
184         /**
185          * Pushes a new element onto the stack. If more then three characters
186          * have been pushed onto the stack the oldest character is removed.
187          *
188          * @param c the character to push onto the stack
189          */
190         public void push(int c) {
191             cache[0] = cache[1];
192             cache[1] = cache[2];
193             cache[2] = c;
194         }
195 
196         /**
197          * Checks if the cache contains the given character sequence.
198          *
199          * @param one character one
200          * @param two character two
201          * @param three character three
202          * @return <code>true</code> if the cache contains the three characters
203          * in order; otherwise <code>false</code>
204          */
205         public boolean checkSequence(char one, char two, char three) {
206             return one == cache[0] && two == cache[1] && three == cache[2];
207         }
208 
209         /**
210          * Checks if the cache contains the given character sequence.
211          *
212          * @param one character one
213          * @param two character two
214          * @return <code>true</code> if the cache contains the two characters in
215          * order; otherwise <code>false</code>
216          */
217         public boolean checkSequence(char one, char two) {
218             return one == cache[1] && two == cache[2];
219         }
220     }
221 }