// 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;
}
}