// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.actions.search; import static org.openstreetmap.josm.tools.I18n.marktr; import static org.openstreetmap.josm.tools.I18n.tr; import java.io.IOException; import java.io.Reader; import java.util.Arrays; import java.util.List; import java.util.Objects; import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError; import org.openstreetmap.josm.tools.JosmRuntimeException; public class PushbackTokenizer { public static class Range { private final long start; private final long end; public Range(long start, long end) { this.start = start; this.end = end; } public long getStart() { return start; } public long getEnd() { return end; } @Override public String toString() { return "Range [start=" + start + ", end=" + end + ']'; } } private final Reader search; private Token currentToken; private String currentText; private Long currentNumber; private Long currentRange; private int c; private boolean isRange; public PushbackTokenizer(Reader search) { this.search = search; getChar(); } public enum Token { NOT(marktr("<not>")), OR(marktr("<or>")), XOR(marktr("<xor>")), LEFT_PARENT(marktr("<left parent>")), RIGHT_PARENT(marktr("<right parent>")), COLON(marktr("<colon>")), EQUALS(marktr("<equals>")), KEY(marktr("<key>")), QUESTION_MARK(marktr("<question mark>")), EOF(marktr("<end-of-file>")), LESS_THAN("<less-than>"), GREATER_THAN("<greater-than>"); Token(String name) { this.name = name; } private final String name; @Override public String toString() { return tr(name); } } private void getChar() { try { c = search.read(); } catch (IOException e) { throw new JosmRuntimeException(e.getMessage(), e); } } private static final List<Character> specialChars = Arrays.asList('"', ':', '(', ')', '|', '^', '=', '?', '<', '>'); private static final List<Character> specialCharsQuoted = Arrays.asList('"'); private String getString(boolean quoted) { List<Character> sChars = quoted ? specialCharsQuoted : specialChars; StringBuilder s = new StringBuilder(); boolean escape = false; while (c != -1 && (escape || (!sChars.contains((char) c) && (quoted || !Character.isWhitespace(c))))) { if (c == '\\' && !escape) { escape = true; } else { s.append((char) c); escape = false; } getChar(); } return s.toString(); } private String getString() { return getString(false); } /** * The token returned is <code>null</code> or starts with an identifier character: * - for an '-'. This will be the only character * : for an key. The value is the next token * | for "OR" * ^ for "XOR" * ' ' for anything else. * @return The next token in the stream. */ public Token nextToken() { if (currentToken != null) { Token result = currentToken; currentToken = null; return result; } while (Character.isWhitespace(c)) { getChar(); } switch (c) { case -1: getChar(); return Token.EOF; case ':': getChar(); return Token.COLON; case '=': getChar(); return Token.EQUALS; case '<': getChar(); return Token.LESS_THAN; case '>': getChar(); return Token.GREATER_THAN; case '(': getChar(); return Token.LEFT_PARENT; case ')': getChar(); return Token.RIGHT_PARENT; case '|': getChar(); return Token.OR; case '^': getChar(); return Token.XOR; case '&': getChar(); return nextToken(); case '?': getChar(); return Token.QUESTION_MARK; case '"': getChar(); currentText = getString(true); getChar(); return Token.KEY; default: String prefix = ""; if (c == '-') { getChar(); if (!Character.isDigit(c)) return Token.NOT; prefix = "-"; } currentText = prefix + getString(); if ("or".equalsIgnoreCase(currentText)) return Token.OR; else if ("xor".equalsIgnoreCase(currentText)) return Token.XOR; else if ("and".equalsIgnoreCase(currentText)) return nextToken(); // try parsing number try { currentNumber = Long.valueOf(currentText); } catch (NumberFormatException e) { currentNumber = null; } // if text contains "-", try parsing a range int pos = currentText.indexOf('-', 1); isRange = pos > 0; if (isRange) { try { currentNumber = Long.valueOf(currentText.substring(0, pos)); } catch (NumberFormatException e) { currentNumber = null; } try { currentRange = Long.valueOf(currentText.substring(pos + 1)); } catch (NumberFormatException e) { currentRange = null; } } else { currentRange = null; } return Token.KEY; } } public boolean readIfEqual(Token token) { Token nextTok = nextToken(); if (Objects.equals(nextTok, token)) return true; currentToken = nextTok; return false; } public String readTextOrNumber() { Token nextTok = nextToken(); if (nextTok == Token.KEY) return currentText; currentToken = nextTok; return null; } public long readNumber(String errorMessage) throws ParseError { if ((nextToken() == Token.KEY) && (currentNumber != null)) return currentNumber; else throw new ParseError(errorMessage); } public long getReadNumber() { return (currentNumber != null) ? currentNumber : 0; } public Range readRange(String errorMessage) throws ParseError { if (nextToken() != Token.KEY || (currentNumber == null && currentRange == null)) { throw new ParseError(errorMessage); } else if (!isRange && currentNumber != null) { if (currentNumber >= 0) { return new Range(currentNumber, currentNumber); } else { return new Range(0, Math.abs(currentNumber)); } } else if (isRange && currentRange == null) { return new Range(currentNumber, Integer.MAX_VALUE); } else if (currentNumber != null && currentRange != null) { return new Range(currentNumber, currentRange); } else { throw new ParseError(errorMessage); } } public String getText() { return currentText; } }