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