001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions.search;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005import static org.openstreetmap.josm.tools.I18n.tr;
006import static org.openstreetmap.josm.tools.Utils.equal;
007
008import java.io.IOException;
009import java.io.Reader;
010import java.util.Arrays;
011import java.util.List;
012
013import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
014
015public class PushbackTokenizer {
016
017    public static class Range {
018        private final long start;
019        private final long end;
020
021        public Range(long start, long end) {
022            this.start = start;
023            this.end = end;
024        }
025
026        public long getStart() {
027            return start;
028        }
029
030        public long getEnd() {
031            return end;
032        }
033
034        /* (non-Javadoc)
035         * @see java.lang.Object#toString()
036         */
037        @Override
038        public String toString() {
039            return "Range [start=" + start + ", end=" + end + "]";
040        }
041    }
042
043    private final Reader search;
044
045    private Token currentToken;
046    private String currentText;
047    private Long currentNumber;
048    private Long currentRange;
049    private int c;
050    private boolean isRange;
051
052    public PushbackTokenizer(Reader search) {
053        this.search = search;
054        getChar();
055    }
056
057    public enum Token {
058        NOT(marktr("<not>")), OR(marktr("<or>")), XOR(marktr("<xor>")), LEFT_PARENT(marktr("<left parent>")),
059        RIGHT_PARENT(marktr("<right parent>")), COLON(marktr("<colon>")), EQUALS(marktr("<equals>")),
060        KEY(marktr("<key>")), QUESTION_MARK(marktr("<question mark>")),
061        EOF(marktr("<end-of-file>")), LESS_THAN("<less-than>"), GREATER_THAN("<greater-than>");
062
063        private Token(String name) {
064            this.name = name;
065        }
066
067        private final String name;
068
069        @Override
070        public String toString() {
071            return tr(name);
072        }
073    }
074
075
076    private void getChar() {
077        try {
078            c = search.read();
079        } catch (IOException e) {
080            throw new RuntimeException(e.getMessage(), e);
081        }
082    }
083
084    private static final List<Character> specialChars = Arrays.asList('"', ':', '(', ')', '|', '^', '=', '?', '<', '>');
085    private static final List<Character> specialCharsQuoted = Arrays.asList('"');
086
087    private String getString(boolean quoted) {
088        List<Character> sChars = quoted ? specialCharsQuoted : specialChars;
089        StringBuilder s = new StringBuilder();
090        boolean escape = false;
091        while (c != -1 && (escape || (!sChars.contains((char)c) && (quoted || !Character.isWhitespace(c))))) {
092            if (c == '\\' && !escape) {
093                escape = true;
094            } else {
095                s.append((char)c);
096                escape = false;
097            }
098            getChar();
099        }
100        return s.toString();
101    }
102
103    private String getString() {
104        return getString(false);
105    }
106
107    /**
108     * The token returned is <code>null</code> or starts with an identifier character:
109     * - for an '-'. This will be the only character
110     * : for an key. The value is the next token
111     * | for "OR"
112     * ^ for "XOR"
113     * ' ' for anything else.
114     * @return The next token in the stream.
115     */
116    public Token nextToken() {
117        if (currentToken != null) {
118            Token result = currentToken;
119            currentToken = null;
120            return result;
121        }
122
123        while (Character.isWhitespace(c)) {
124            getChar();
125        }
126        switch (c) {
127        case -1:
128            getChar();
129            return Token.EOF;
130        case ':':
131            getChar();
132            return Token.COLON;
133        case '=':
134            getChar();
135            return Token.EQUALS;
136        case '<':
137            getChar();
138            return Token.LESS_THAN;
139        case '>':
140            getChar();
141            return Token.GREATER_THAN;
142        case '(':
143            getChar();
144            return Token.LEFT_PARENT;
145        case ')':
146            getChar();
147            return Token.RIGHT_PARENT;
148        case '|':
149            getChar();
150            return Token.OR;
151        case '^':
152            getChar();
153            return Token.XOR;
154        case '&':
155            getChar();
156            return nextToken();
157        case '?':
158            getChar();
159            return Token.QUESTION_MARK;
160        case '"':
161            getChar();
162            currentText = getString(true);
163            getChar();
164            return Token.KEY;
165        default:
166            String prefix = "";
167            if (c == '-') {
168                getChar();
169                if (!Character.isDigit(c))
170                    return Token.NOT;
171                prefix = "-";
172            }
173            currentText = prefix + getString();
174            if ("or".equalsIgnoreCase(currentText))
175                return Token.OR;
176            else if ("xor".equalsIgnoreCase(currentText))
177                return Token.XOR;
178            else if ("and".equalsIgnoreCase(currentText))
179                return nextToken();
180            // try parsing number
181            try {
182                currentNumber = Long.parseLong(currentText);
183            } catch (NumberFormatException e) {
184                currentNumber = null;
185            }
186            // if text contains "-", try parsing a range
187            int pos = currentText.indexOf('-', 1);
188            isRange = pos > 0;
189            if (isRange) {
190                try {
191                    currentNumber = Long.parseLong(currentText.substring(0, pos));
192                } catch (NumberFormatException e) {
193                    currentNumber = null;
194                }
195                try {
196                    currentRange = Long.parseLong(currentText.substring(pos + 1));
197                } catch (NumberFormatException e) {
198                    currentRange = null;
199                    }
200                } else {
201                    currentRange = null;
202                }
203            return Token.KEY;
204        }
205    }
206
207    public boolean readIfEqual(Token token) {
208        Token nextTok = nextToken();
209        if (equal(nextTok, token))
210            return true;
211        currentToken = nextTok;
212        return false;
213    }
214
215    public String readTextOrNumber() {
216        Token nextTok = nextToken();
217        if (nextTok == Token.KEY)
218            return currentText;
219        currentToken = nextTok;
220        return null;
221    }
222
223    public long readNumber(String errorMessage) throws ParseError {
224        if ((nextToken() == Token.KEY) && (currentNumber != null))
225            return currentNumber;
226        else
227            throw new ParseError(errorMessage);
228    }
229
230    public long getReadNumber() {
231        return (currentNumber != null) ? currentNumber : 0;
232    }
233
234    public Range readRange(String errorMessage) throws ParseError {
235        if (nextToken() != Token.KEY || (currentNumber == null && currentRange == null)) {
236            throw new ParseError(errorMessage);
237        } else if (!isRange && currentNumber != null) {
238            if (currentNumber >= 0) {
239                return new Range(currentNumber, currentNumber);
240            } else {
241                return new Range(0, Math.abs(currentNumber));
242            }
243        } else if (isRange && currentRange == null) {
244            return new Range(currentNumber, Integer.MAX_VALUE);
245        } else if (currentNumber != null && currentRange != null) {
246            return new Range(currentNumber, currentRange);
247        } else {
248            throw new ParseError(errorMessage);
249        }
250    }
251
252    public String getText() {
253        return currentText;
254    }
255}