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