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