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