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