package de.westnordost.streetcomplete.data.osm.tql;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
/**
* Compiles a string in filter syntax into a TagFilterExpression. A string in filter syntax is
* something like this:
*
* <tt>"ways with (highway = residential or highway = tertiary) and !name"</tt> (finds all
* residential and tertiary roads that have no name)
*/
public class FiltersParser
{
private static final char[] QUOTATION_MARKS = {'"', '\''};
private static final String[] OPERATORS = {"=", "!=", "~", "!~"};
private static final String WITH = "with";
private static final String OR = "or";
private static final String AND = "and";
private static final String AROUND = "around";
private static final String[] RESERVED_WORDS = {WITH, OR, AND, AROUND};
private StringWithCursor input;
public TagFilterExpression parse(String input)
{
try
{
// convert all white-spacey things to whitespaces so we do not have to deal with them later
this.input = new StringWithCursor(input.replaceAll("\\s", " "));
List<ElementsTypeFilter> elementsTypeFilters = parseElementsDeclaration();
BooleanExpression<OQLExpressionValue> tagExprRoot = parseTags();
return new TagFilterExpression(elementsTypeFilters, tagExprRoot);
}
catch(ParseException e)
{
throw new RuntimeException(e);
}
}
private int expectAnyNumberOfSpaces()
{
int count = 0;
while(input.nextIsAndAdvance(' ')) count++;
return count;
}
private int expectOneOrMoreSpaces() throws ParseException
{
if(!input.nextIsAndAdvance(' '))
throw new ParseException("Expected a whitespace", input.getCursorPos());
return expectAnyNumberOfSpaces() + 1;
}
private List<ElementsTypeFilter> parseElementsDeclaration() throws ParseException
{
List<ElementsTypeFilter> result = new ArrayList<>();
result.add(parseElementDeclaration());
while(input.nextIsAndAdvance(','))
{
ElementsTypeFilter element = parseElementDeclaration();
if(result.contains(element))
{
throw new ParseException("Mentioned the same element type " + element + " twice",
input.getCursorPos());
}
result.add(element);
}
return result;
}
private ElementsTypeFilter parseElementDeclaration() throws ParseException
{
expectAnyNumberOfSpaces();
for(ElementsTypeFilter t : ElementsTypeFilter.values())
{
if(input.nextIsAndAdvanceIgnoreCase(t.name))
{
expectAnyNumberOfSpaces();
return t;
}
}
throw new ParseException("Expected element types." +
"Any of: nodes, ways or relations, separated by ','", input.getCursorPos());
}
private BooleanExpression<OQLExpressionValue> parseTags() throws ParseException
{
// tags are optional...
if(!input.nextIsAndAdvanceIgnoreCase(WITH))
{
if(!input.isAtEnd())
{
throw new ParseException("Expected end of string or 'with' keyword",
input.getCursorPos());
}
return new BooleanExpression<>();
}
BooleanExpressionBuilder<OQLExpressionValue> builder = new BooleanExpressionBuilder<>();
do
{
if(!parseBrackets('(', builder))
// if it has no bracket, there must be at least one whitespace
throw new ParseException("Expected a whitespace or bracket before the tag",
input.getCursorPos());
builder.addValue(parseTag());
// parseTag() might have "eaten up" a whitespace after the key in expectation of an
// operator.
boolean separated = input.previousIs(' ');
separated |= parseBrackets(')', builder);
if(input.isAtEnd()) break;
if(!separated)
// same as with the opening bracket, only that if the string is over, its okay
throw new ParseException("Expected a whitespace or bracket after the tag",
input.getCursorPos());
if (input.nextIsAndAdvanceIgnoreCase(OR))
{
builder.addOr();
}
else if (input.nextIsAndAdvanceIgnoreCase(AND))
{
builder.addAnd();
}
else
throw new ParseException("Expected end of string, 'and' or 'or'",
input.getCursorPos());
} while(true);
try
{
return builder.getResult();
}
catch(IllegalStateException e)
{
throw new ParseException(e.getMessage(), input.getCursorPos());
}
}
private boolean parseBrackets(char bracket, BooleanExpressionBuilder expr) throws ParseException
{
int characterCount = expectAnyNumberOfSpaces();
int previousCharacterCount;
do
{
previousCharacterCount = characterCount;
if (input.nextIsAndAdvance(bracket))
{
try
{
if (bracket == '(') expr.addOpenBracket();
else if (bracket == ')') expr.addCloseBracket();
}
catch(IllegalStateException e)
{
throw new ParseException(e.getMessage(), input.getCursorPos());
}
characterCount++;
}
characterCount += expectAnyNumberOfSpaces();
}
while(characterCount > previousCharacterCount);
return characterCount > 0;
}
private TagFilterValue parseTag() throws ParseException
{
String operator = null;
String value = null;
// !key at the start is translated to key!~.*
if(input.nextIsAndAdvance('!'))
{
// Overpass understands "." to mean "any string". For a Java regex matcher, that would
// be ".*"
operator = "!~";
value = ".*";
}
String key = parseKey();
expectAnyNumberOfSpaces();
String binaryOperator = parseOperator();
if(binaryOperator != null)
{
if(operator == null)
operator = binaryOperator;
else
throw new ParseException("Tried to use the '"+binaryOperator+"' operator while " +
"already using the unary negation operator", input.getCursorPos());
}
// without operator, we do not expect a value
if(operator != null && value == null)
{
expectAnyNumberOfSpaces();
value = parseValue();
}
return new TagFilterValue(key, operator, value);
}
private String parseKey() throws ParseException
{
String reserved = nextIsReservedWord();
if(reserved != null)
throw new ParseException("A key cannot be named like the reserved word '" +
reserved + "', you must surround the key with quotation marks",
input.getCursorPos());
int length = findKeyLength();
if(length == 0)
{
throw new ParseException("Missing key (dangling boolean operator)", input.getCursorPos());
}
return input.advanceBy(length);
}
private String parseOperator()
{
for(String o : OPERATORS)
{
if(input.nextIsAndAdvance(o)) return o;
}
return null;
}
private String parseValue() throws ParseException
{
int length = findValueLength();
if(length == 0)
{
throw new ParseException("Missing value (dangling operator)", input.getCursorPos());
}
return input.advanceBy(length);
}
private int findKeyLength() throws ParseException
{
Integer length = findQuotationLength();
if(length != null) return length;
length = Math.min(input.findNext(' '), input.findNext(')'));
for(String o : OPERATORS)
{
int opLen = input.findNext(o);
if(opLen < length) length = opLen;
}
return length;
}
private int findValueLength() throws ParseException
{
Integer length = findQuotationLength();
if(length != null) return length;
return Math.min(input.findNext(' '), input.findNext(')'));
}
private Integer findQuotationLength() throws ParseException
{
for (char quot : QUOTATION_MARKS)
{
if (input.nextIs(quot))
{
int length = input.findNext(quot,1);
if(input.isAtEnd(length))
throw new ParseException("Did not close quotation marks",input.getCursorPos()-1);
// +1 because we want to include teh closing quotation mark
return length+1;
}
}
return null;
}
private String nextIsReservedWord()
{
for (String reserved : RESERVED_WORDS)
{
if(input.nextIsIgnoreCase(reserved)) return reserved;
}
return null;
}
}