1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: H2 Group |
5 | */ |
6 | package org.h2.util; |
7 | |
8 | import java.io.Closeable; |
9 | import java.io.IOException; |
10 | import java.io.Reader; |
11 | import java.util.Arrays; |
12 | import org.h2.engine.Constants; |
13 | import org.h2.message.DbException; |
14 | |
15 | /** |
16 | * This class can split SQL scripts to single SQL statements. |
17 | * Each SQL statement ends with the character ';', however it is ignored |
18 | * in comments and quotes. |
19 | */ |
20 | public class ScriptReader implements Closeable { |
21 | |
22 | private final Reader reader; |
23 | private char[] buffer; |
24 | |
25 | /** |
26 | * The position in the buffer of the next char to be read |
27 | */ |
28 | private int bufferPos; |
29 | |
30 | /** |
31 | * The position in the buffer of the statement start |
32 | */ |
33 | private int bufferStart = -1; |
34 | |
35 | /** |
36 | * The position in the buffer of the last available char |
37 | */ |
38 | private int bufferEnd; |
39 | |
40 | /** |
41 | * True if we have read past the end of file |
42 | */ |
43 | private boolean endOfFile; |
44 | |
45 | /** |
46 | * True if we are inside a comment |
47 | */ |
48 | private boolean insideRemark; |
49 | |
50 | /** |
51 | * Only valid if insideRemark is true. True if we are inside a block |
52 | * comment, false if we are inside a line comment |
53 | */ |
54 | private boolean blockRemark; |
55 | |
56 | /** |
57 | * True if comments should be skipped completely by this reader. |
58 | */ |
59 | private boolean skipRemarks; |
60 | |
61 | /** |
62 | * The position in buffer of start of comment |
63 | */ |
64 | private int remarkStart; |
65 | |
66 | /** |
67 | * Create a new SQL script reader from the given reader |
68 | * |
69 | * @param reader the reader |
70 | */ |
71 | public ScriptReader(Reader reader) { |
72 | this.reader = reader; |
73 | buffer = new char[Constants.IO_BUFFER_SIZE * 2]; |
74 | } |
75 | |
76 | /** |
77 | * Close the underlying reader. |
78 | */ |
79 | @Override |
80 | public void close() { |
81 | try { |
82 | reader.close(); |
83 | } catch (IOException e) { |
84 | throw DbException.convertIOException(e, null); |
85 | } |
86 | } |
87 | |
88 | /** |
89 | * Read a statement from the reader. This method returns null if the end has |
90 | * been reached. |
91 | * |
92 | * @return the SQL statement or null |
93 | */ |
94 | public String readStatement() { |
95 | if (endOfFile) { |
96 | return null; |
97 | } |
98 | try { |
99 | return readStatementLoop(); |
100 | } catch (IOException e) { |
101 | throw DbException.convertIOException(e, null); |
102 | } |
103 | } |
104 | |
105 | private String readStatementLoop() throws IOException { |
106 | bufferStart = bufferPos; |
107 | int c = read(); |
108 | while (true) { |
109 | if (c < 0) { |
110 | endOfFile = true; |
111 | if (bufferPos - 1 == bufferStart) { |
112 | return null; |
113 | } |
114 | break; |
115 | } else if (c == ';') { |
116 | break; |
117 | } |
118 | switch (c) { |
119 | case '$': { |
120 | c = read(); |
121 | if (c == '$' && (bufferPos - bufferStart < 3 || buffer[bufferPos - 3] <= ' ')) { |
122 | // dollar quoted string |
123 | while (true) { |
124 | c = read(); |
125 | if (c < 0) { |
126 | break; |
127 | } |
128 | if (c == '$') { |
129 | c = read(); |
130 | if (c < 0) { |
131 | break; |
132 | } |
133 | if (c == '$') { |
134 | break; |
135 | } |
136 | } |
137 | } |
138 | c = read(); |
139 | } |
140 | break; |
141 | } |
142 | case '\'': |
143 | while (true) { |
144 | c = read(); |
145 | if (c < 0) { |
146 | break; |
147 | } |
148 | if (c == '\'') { |
149 | break; |
150 | } |
151 | } |
152 | c = read(); |
153 | break; |
154 | case '"': |
155 | while (true) { |
156 | c = read(); |
157 | if (c < 0) { |
158 | break; |
159 | } |
160 | if (c == '\"') { |
161 | break; |
162 | } |
163 | } |
164 | c = read(); |
165 | break; |
166 | case '/': { |
167 | c = read(); |
168 | if (c == '*') { |
169 | // block comment |
170 | startRemark(true); |
171 | while (true) { |
172 | c = read(); |
173 | if (c < 0) { |
174 | break; |
175 | } |
176 | if (c == '*') { |
177 | c = read(); |
178 | if (c < 0) { |
179 | clearRemark(); |
180 | break; |
181 | } |
182 | if (c == '/') { |
183 | endRemark(); |
184 | break; |
185 | } |
186 | } |
187 | } |
188 | c = read(); |
189 | } else if (c == '/') { |
190 | // single line comment |
191 | startRemark(false); |
192 | while (true) { |
193 | c = read(); |
194 | if (c < 0) { |
195 | clearRemark(); |
196 | break; |
197 | } |
198 | if (c == '\r' || c == '\n') { |
199 | endRemark(); |
200 | break; |
201 | } |
202 | } |
203 | c = read(); |
204 | } |
205 | break; |
206 | } |
207 | case '-': { |
208 | c = read(); |
209 | if (c == '-') { |
210 | // single line comment |
211 | startRemark(false); |
212 | while (true) { |
213 | c = read(); |
214 | if (c < 0) { |
215 | clearRemark(); |
216 | break; |
217 | } |
218 | if (c == '\r' || c == '\n') { |
219 | endRemark(); |
220 | break; |
221 | } |
222 | } |
223 | c = read(); |
224 | } |
225 | break; |
226 | } |
227 | default: { |
228 | c = read(); |
229 | } |
230 | } |
231 | } |
232 | return new String(buffer, bufferStart, bufferPos - 1 - bufferStart); |
233 | } |
234 | |
235 | private void startRemark(boolean block) { |
236 | blockRemark = block; |
237 | remarkStart = bufferPos - 2; |
238 | insideRemark = true; |
239 | } |
240 | |
241 | private void endRemark() { |
242 | clearRemark(); |
243 | insideRemark = false; |
244 | } |
245 | |
246 | private void clearRemark() { |
247 | if (skipRemarks) { |
248 | Arrays.fill(buffer, remarkStart, bufferPos, ' '); |
249 | } |
250 | } |
251 | |
252 | private int read() throws IOException { |
253 | if (bufferPos >= bufferEnd) { |
254 | return readBuffer(); |
255 | } |
256 | return buffer[bufferPos++]; |
257 | } |
258 | |
259 | private int readBuffer() throws IOException { |
260 | if (endOfFile) { |
261 | return -1; |
262 | } |
263 | int keep = bufferPos - bufferStart; |
264 | if (keep > 0) { |
265 | char[] src = buffer; |
266 | if (keep + Constants.IO_BUFFER_SIZE > src.length) { |
267 | // protect against NegativeArraySizeException |
268 | if (src.length >= Integer.MAX_VALUE / 2) { |
269 | throw new IOException("Error in parsing script, " + |
270 | "statement size exceeds 1G, " + |
271 | "first 80 characters of statement looks like: " + |
272 | new String(buffer, bufferStart, 80)); |
273 | } |
274 | buffer = new char[src.length * 2]; |
275 | } |
276 | System.arraycopy(src, bufferStart, buffer, 0, keep); |
277 | } |
278 | remarkStart -= bufferStart; |
279 | bufferStart = 0; |
280 | bufferPos = keep; |
281 | int len = reader.read(buffer, keep, Constants.IO_BUFFER_SIZE); |
282 | if (len == -1) { |
283 | // ensure bufferPos > bufferEnd |
284 | bufferEnd = -1024; |
285 | endOfFile = true; |
286 | // ensure the right number of characters are read |
287 | // in case the input buffer is still used |
288 | bufferPos++; |
289 | return -1; |
290 | } |
291 | bufferEnd = keep + len; |
292 | return buffer[bufferPos++]; |
293 | } |
294 | |
295 | /** |
296 | * Check if this is the last statement, and if the single line or block |
297 | * comment is not finished yet. |
298 | * |
299 | * @return true if the current position is inside a remark |
300 | */ |
301 | public boolean isInsideRemark() { |
302 | return insideRemark; |
303 | } |
304 | |
305 | /** |
306 | * If currently inside a remark, this method tells if it is a block comment |
307 | * (true) or single line comment (false) |
308 | * |
309 | * @return true if inside a block comment |
310 | */ |
311 | public boolean isBlockRemark() { |
312 | return blockRemark; |
313 | } |
314 | |
315 | /** |
316 | * If comments should be skipped completely by this reader. |
317 | * |
318 | * @param skipRemarks true if comments should be skipped |
319 | */ |
320 | public void setSkipRemarks(boolean skipRemarks) { |
321 | this.skipRemarks = skipRemarks; |
322 | } |
323 | |
324 | } |