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