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