001 /*
002 // $Id: ParseRegion.java 482 2012-01-05 23:27:27Z jhyde $
003 //
004 // Licensed to Julian Hyde under one or more contributor license
005 // agreements. See the NOTICE file distributed with this work for
006 // additional information regarding copyright ownership.
007 //
008 // Julian Hyde licenses this file to you under the Apache License,
009 // Version 2.0 (the "License"); you may not use this file except in
010 // compliance with the License. You may obtain a copy of the License at:
011 //
012 // http://www.apache.org/licenses/LICENSE-2.0
013 //
014 // Unless required by applicable law or agreed to in writing, software
015 // distributed under the License is distributed on an "AS IS" BASIS,
016 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 // See the License for the specific language governing permissions and
018 // limitations under the License.
019 */
020 package org.olap4j.mdx;
021
022 /**
023 * Region of parser source code.
024 *
025 * <p>The main purpose of a ParseRegion is to give detailed locations in
026 * error messages and warnings from the parsing and validation process.
027 *
028 * <p>A region has a start and end line number and column number. A region is
029 * a point if the start and end positions are the same.
030 *
031 * <p>The line and column number are one-based, because that is what end-users
032 * understand.
033 *
034 * <p>A region's end-points are inclusive. For example, in the code
035 *
036 * <blockquote><pre>SELECT FROM [Sales]</pre></blockquote>
037 *
038 * the <code>SELECT</code> token has region [1:1, 1:6].
039 *
040 * <p>Regions are immutable.
041 *
042 * @version $Id: ParseRegion.java 482 2012-01-05 23:27:27Z jhyde $
043 * @author jhyde
044 */
045 public class ParseRegion {
046 private final int startLine;
047 private final int startColumn;
048 private final int endLine;
049 private final int endColumn;
050
051 private static final String NL = System.getProperty("line.separator");
052
053 /**
054 * Creates a ParseRegion.
055 *
056 * <p>All lines and columns are 1-based and inclusive. For example, the
057 * token "select" in "select from [Sales]" has a region [1:1, 1:6].
058 *
059 * @param startLine Line of the beginning of the region
060 * @param startColumn Column of the beginning of the region
061 * @param endLine Line of the end of the region
062 * @param endColumn Column of the end of the region
063 */
064 public ParseRegion(
065 int startLine,
066 int startColumn,
067 int endLine,
068 int endColumn)
069 {
070 assert endLine >= startLine;
071 assert endLine > startLine || endColumn >= startColumn;
072 this.startLine = startLine;
073 this.startColumn = startColumn;
074 this.endLine = endLine;
075 this.endColumn = endColumn;
076 }
077
078 /**
079 * Creates a ParseRegion.
080 *
081 * All lines and columns are 1-based.
082 *
083 * @param line Line of the beginning and end of the region
084 * @param column Column of the beginning and end of the region
085 */
086 public ParseRegion(
087 int line,
088 int column)
089 {
090 this(line, column, line, column);
091 }
092
093 /**
094 * Return starting line number (1-based).
095 *
096 * @return 1-based starting line number
097 */
098 public int getStartLine() {
099 return startLine;
100 }
101
102 /**
103 * Return starting column number (1-based).
104 *
105 * @return 1-based starting column number
106 */
107 public int getStartColumn() {
108 return startColumn;
109 }
110
111 /**
112 * Return ending line number (1-based).
113 *
114 * @return 1-based ending line number
115 */
116 public int getEndLine() {
117 return endLine;
118 }
119
120 /**
121 * Return ending column number (1-based).
122 *
123 * @return 1-based starting endings column number
124 */
125 public int getEndColumn() {
126 return endColumn;
127 }
128
129 /**
130 * Returns a string representation of this ParseRegion.
131 *
132 * <p>Regions are of the form
133 * <code>[startLine:startColumn, endLine:endColumn]</code>, or
134 * <code>[startLine:startColumn]</code> for point regions.
135 *
136 * @return string representation of this ParseRegion
137 */
138 public String toString() {
139 return "[" + startLine + ":" + startColumn
140 + ((isPoint())
141 ? ""
142 : ", " + endLine + ":" + endColumn)
143 + "]";
144 }
145
146 /**
147 * Returns whether this region has the same start and end point.
148 *
149 * @return whether this region has the same start and end point
150 */
151 public boolean isPoint() {
152 return endLine == startLine && endColumn == startColumn;
153 }
154
155 public int hashCode() {
156 return startLine ^
157 (startColumn << 2) ^
158 (endLine << 4) ^
159 (endColumn << 8);
160 }
161
162 public boolean equals(Object obj) {
163 if (obj instanceof ParseRegion) {
164 final ParseRegion that = (ParseRegion) obj;
165 return this.startLine == that.startLine
166 && this.startColumn == that.startColumn
167 && this.endLine == that.endLine
168 && this.endColumn == that.endColumn;
169 } else {
170 return false;
171 }
172 }
173
174 /**
175 * Combines this region with a list of parse tree nodes to create a
176 * region which spans from the first point in the first to the last point
177 * in the other.
178 *
179 * @param regions Collection of source code regions
180 * @return region which represents the span of the given regions
181 */
182 public ParseRegion plusAll(Iterable<ParseRegion> regions)
183 {
184 return sum(
185 regions,
186 getStartLine(),
187 getStartColumn(),
188 getEndLine(),
189 getEndColumn());
190 }
191
192 /**
193 * Combines the parser positions of a list of nodes to create a position
194 * which spans from the beginning of the first to the end of the last.
195 *
196 * @param nodes Collection of parse tree nodes
197 * @return region which represents the span of the given nodes
198 */
199 public static ParseRegion sum(
200 Iterable<ParseRegion> nodes)
201 {
202 return sum(nodes, Integer.MAX_VALUE, Integer.MAX_VALUE, -1, -1);
203 }
204
205 private static ParseRegion sum(
206 Iterable<ParseRegion> regions,
207 int startLine,
208 int startColumn,
209 int endLine,
210 int endColumn)
211 {
212 int testLine;
213 int testColumn;
214 for (ParseRegion region : regions) {
215 if (region == null) {
216 continue;
217 }
218 testLine = region.getStartLine();
219 testColumn = region.getStartColumn();
220 if ((testLine < startLine)
221 || ((testLine == startLine) && (testColumn < startColumn)))
222 {
223 startLine = testLine;
224 startColumn = testColumn;
225 }
226
227 testLine = region.getEndLine();
228 testColumn = region.getEndColumn();
229 if ((testLine > endLine)
230 || ((testLine == endLine) && (testColumn > endColumn)))
231 {
232 endLine = testLine;
233 endColumn = testColumn;
234 }
235 }
236 return new ParseRegion(startLine, startColumn, endLine, endColumn);
237 }
238
239 /**
240 * Looks for one or two carets in an MDX string, and if present, converts
241 * them into a parser position.
242 *
243 * <p>Examples:
244 *
245 * <ul>
246 * <li>findPos("xxx^yyy") yields {"xxxyyy", position 3, line 1 column 4}
247 * <li>findPos("xxxyyy") yields {"xxxyyy", null}
248 * <li>findPos("xxx^yy^y") yields {"xxxyyy", position 3, line 4 column 4
249 * through line 1 column 6}
250 * </ul>
251 *
252 * @param code Source code
253 * @return object containing source code annotated with region
254 */
255 public static RegionAndSource findPos(String code)
256 {
257 int firstCaret = code.indexOf('^');
258 if (firstCaret < 0) {
259 return new RegionAndSource(code, null);
260 }
261 int secondCaret = code.indexOf('^', firstCaret + 1);
262 if (secondCaret < 0) {
263 String codeSansCaret =
264 code.substring(0, firstCaret)
265 + code.substring(firstCaret + 1);
266 int [] start = indexToLineCol(code, firstCaret);
267 return new RegionAndSource(
268 codeSansCaret,
269 new ParseRegion(start[0], start[1]));
270 } else {
271 String codeSansCaret =
272 code.substring(0, firstCaret)
273 + code.substring(firstCaret + 1, secondCaret)
274 + code.substring(secondCaret + 1);
275 int [] start = indexToLineCol(code, firstCaret);
276
277 // subtract 1 because first caret pushed the string out
278 --secondCaret;
279
280 // subtract 1 because the col position needs to be inclusive
281 --secondCaret;
282 int [] end = indexToLineCol(code, secondCaret);
283 return new RegionAndSource(
284 codeSansCaret,
285 new ParseRegion(start[0], start[1], end[0], end[1]));
286 }
287 }
288
289 /**
290 * Returns the (1-based) line and column corresponding to a particular
291 * (0-based) offset in a string.
292 *
293 * <p>Converse of {@link #lineColToIndex(String, int, int)}.
294 *
295 * @param code Source code
296 * @param i Offset within source code
297 * @return 2-element array containing line and column
298 */
299 private static int [] indexToLineCol(String code, int i) {
300 int line = 0;
301 int j = 0;
302 while (true) {
303 String s;
304 int rn = code.indexOf("\r\n", j);
305 int r = code.indexOf("\r", j);
306 int n = code.indexOf("\n", j);
307 int prevj = j;
308 if ((r < 0) && (n < 0)) {
309 assert rn < 0;
310 s = null;
311 j = -1;
312 } else if ((rn >= 0) && (rn < n) && (rn <= r)) {
313 s = "\r\n";
314 j = rn;
315 } else if ((r >= 0) && (r < n)) {
316 s = "\r";
317 j = r;
318 } else {
319 s = "\n";
320 j = n;
321 }
322 if ((j < 0) || (j > i)) {
323 return new int[] { line + 1, i - prevj + 1 };
324 }
325 assert s != null;
326 j += s.length();
327 ++line;
328 }
329 }
330
331 /**
332 * Finds the position (0-based) in a string which corresponds to a given
333 * line and column (1-based).
334 *
335 * <p>Converse of {@link #indexToLineCol(String, int)}.
336 *
337 * @param code Source code
338 * @param line Line number
339 * @param column Column number
340 * @return Offset within source code
341 */
342 private static int lineColToIndex(String code, int line, int column)
343 {
344 --line;
345 --column;
346 int i = 0;
347 while (line-- > 0) {
348 // Works on linux where line ending is "\n";
349 // also works on windows where line ending is "\r\n".
350 // Even works if they supply linux strings on windows.
351 i = code.indexOf("\n", i)
352 + "\n".length();
353 }
354 return i + column;
355 }
356
357 /**
358 * Generates a string of the source code annotated with caret symbols ("^")
359 * at the beginning and end of the region.
360 *
361 * <p>For example, for the region <code>(1, 9, 1, 12)</code> and source
362 * <code>"values (foo)"</code>,
363 * yields the string <code>"values (^foo^)"</code>.
364 *
365 * @param source Source code
366 * @return Source code annotated with position
367 */
368 public String annotate(String source) {
369 return addCarets(source, startLine, startColumn, endLine, endColumn);
370 }
371
372 /**
373 * Converts a string to a string with one or two carets in it. For example,
374 * <code>addCarets("values (foo)", 1, 9, 1, 11)</code> yields "values
375 * (^foo^)".
376 *
377 * @param sql Source code
378 * @param line Line number
379 * @param col Column number
380 * @param endLine Line number of end of region
381 * @param endCol Column number of end of region
382 * @return String annotated with region
383 */
384 private static String addCarets(
385 String sql,
386 int line,
387 int col,
388 int endLine,
389 int endCol)
390 {
391 String sqlWithCarets;
392 int cut = lineColToIndex(sql, line, col);
393 sqlWithCarets = sql.substring(0, cut) + "^"
394 + sql.substring(cut);
395 if ((col != endCol) || (line != endLine)) {
396 cut = lineColToIndex(sqlWithCarets, endLine, endCol + 1);
397 ++cut; // for caret
398 if (cut < sqlWithCarets.length()) {
399 sqlWithCarets =
400 sqlWithCarets.substring(0, cut)
401 + "^" + sqlWithCarets.substring(cut);
402 } else {
403 sqlWithCarets += "^";
404 }
405 }
406 return sqlWithCarets;
407 }
408
409 /**
410 * Combination of a region within an MDX statement with the source text
411 * of the whole MDX statement.
412 *
413 * <p>Useful for reporting errors. For example, the error in the statement
414 *
415 * <blockquote>
416 * <pre>
417 * SELECT {<b><i>[Measures].[Units In Stock]</i></b>} ON COLUMNS
418 * FROM [Sales]
419 * </pre>
420 * </blockquote>
421 *
422 * has source
423 * "SELECT {[Measures].[Units In Stock]} ON COLUMNS\nFROM [Sales]" and
424 * region [1:9, 1:34].
425 */
426 public static class RegionAndSource {
427 public final String source;
428 public final ParseRegion region;
429
430 /**
431 * Creates a RegionAndSource.
432 *
433 * @param source Source MDX code
434 * @param region Coordinates of region within MDX code
435 */
436 public RegionAndSource(String source, ParseRegion region) {
437 this.source = source;
438 this.region = region;
439 }
440 }
441 }
442
443 // End ParseRegion.java