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 }