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