/* * Copyright (C) 2013 * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * */ package uk.me.parabola.mkgmap.osmstyle.eval; import java.util.HashSet; import java.util.Set; import java.util.Stack; import uk.me.parabola.log.Logger; import uk.me.parabola.mkgmap.osmstyle.function.FunctionFactory; import uk.me.parabola.mkgmap.osmstyle.function.GetTagFunction; import uk.me.parabola.mkgmap.osmstyle.function.StyleFunction; import uk.me.parabola.mkgmap.reader.osm.FeatureKind; import uk.me.parabola.mkgmap.scan.SyntaxException; import uk.me.parabola.mkgmap.scan.TokenScanner; import uk.me.parabola.mkgmap.scan.WordInfo; import static uk.me.parabola.mkgmap.osmstyle.eval.NodeType.*; /** * Read an expression from a style file. */ public class ExpressionReader { private static final Logger log = Logger.getLogger(ExpressionReader.class); private final Stack<Op> stack = new Stack<Op>(); private final Stack<Op> opStack = new Stack<Op>(); private final TokenScanner scanner; private final FeatureKind kind; private final Set<String> usedTags = new HashSet<String>(); public ExpressionReader(TokenScanner scanner, FeatureKind kind) { this.scanner = scanner; this.kind = kind; } /** * Read the conditions. They are terminated by a '[' or '{' character * or by end of file. */ public Op readConditions() { while (!scanner.isEndOfFile()) { scanner.skipSpace(); if (scanner.checkToken("[") || scanner.checkToken("{")) break; WordInfo wordInfo = scanner.nextWordWithInfo(); if (isOperation(wordInfo)) { saveOp(wordInfo.getText()); } else if (wordInfo.isQuoted()) { pushValue(wordInfo.getText()); } else if (wordInfo.getText().charAt(0) == '$') { String tagname = scanner.nextWord(); if (tagname.equals("{")) { tagname = scanner.nextWord(); scanner.validateNext("}"); } stack.push(new GetTagFunction(tagname)); } else if (scanner.checkToken("(")) { // it is a function // this requires a () after the function name scanner.validateNext("("); scanner.validateNext(")"); saveFunction(wordInfo.getText()); } else { pushValue(wordInfo.getText()); } } // Complete building the tree while (!opStack.isEmpty()) runOp(scanner); // The stack should contain one entry which is the complete tree if (stack.size() != 1) throw new SyntaxException(scanner, "Stack size is "+stack.size()); assert stack.size() == 1; Op op = stack.pop(); if (op instanceof ValueOp) throw new SyntaxException(scanner, "Incomplete expression, just a single symbol: " + op); return op; } /** * Is this a token representing an operation? * @param token The string to test. * @return True if this looks like an operator. */ private boolean isOperation(WordInfo token) { // A quoted word is not an operator eg: '=' is a string. if (token.isQuoted()) return false; // Quick check, operators are 1 or 2 characters long. String text = token.getText(); if (text.length() > 2 || text.isEmpty()) return false; // If first character is an operation character then it is an operator // (or a syntax error) char first = text.charAt(0); String chars = "&|!=~()><"; return chars.indexOf(first) >= 0; } /** * Tags used in all the expressions in this file. * @return A set of tag names. */ public Set<String> getUsedTags() { return usedTags; } /** * An operation is saved on the operation stack. The tree is built * as operations of different priorities arrive. */ private void saveOp(String value) { log.debug("save op", value); if (value.equals("#")) { scanner.skipLine(); return; } Op op; try { op = AbstractOp.createOp(value); while (!opStack.isEmpty() && opStack.peek().hasHigherPriority(op)) runOp(scanner); } catch (SyntaxException e) { throw new SyntaxException(scanner, e.getRawMessage()); } if (op.getType() == CLOSE_PAREN) { // Check that there was an opening parenthesis and remove it if (opStack.isEmpty() || !opStack.peek().isType(OPEN_PAREN)) throw new SyntaxException(scanner, "No matching open parenthesis"); opStack.pop(); } else { opStack.push(op); } } /** * Combine the operation at the top of its stack with its values. * @param scanner The token scanner; used for line numbers. */ private void runOp(TokenScanner scanner) { Op op = opStack.pop(); log.debug("Running op...", op.getType()); if (op instanceof BinaryOp) { if (stack.size() < 2) { throw new SyntaxException(scanner, String.format("Not enough arguments for '%s' operator", op.getType().toSymbol())); } Op arg2 = stack.pop(); Op arg1 = stack.pop(); if (arg1.isType(VALUE) /*&& arg2.isType(VALUE)*/) arg1 = new GetTagFunction(arg1.getKeyValue()); BinaryOp binaryOp = (BinaryOp) op; binaryOp.setFirst(arg1); binaryOp.setSecond(arg2); // Deal with the case where you have: a & b=2. The 'a' is a syntax error in this case. if (op.isType(OR) || op.isType(AND)) { if (arg1.isType(VALUE) || arg1.isType(FUNCTION)) throw new SyntaxException(scanner, String.format("Value '%s' is not part of an expression", arg1)); if (arg2.isType(VALUE) || arg2.isType(FUNCTION)) throw new SyntaxException(scanner, String.format("Value '%s' is not part of an expression", arg2)); } else { // All binary ops other than OR and AND take two values. A function is a value // type too. if (!(arg1.isType(VALUE) || arg1.isType(FUNCTION)) || !(arg2.isType(VALUE) || arg2.isType(FUNCTION))) { String msg = String.format("Invalid arguments to %s: %s (%s) and %s (%s)", op.getType(), arg1.getType(), arg1, arg2.getType(), arg2); throw new SyntaxException(scanner, msg); } } // The combination foo=* is converted to exists(foo). if (op.isType(EQUALS) && arg2.isType(VALUE) && ((ValueOp) arg2).isValue("*")) { log.debug("convert to EXISTS"); op = new ExistsOp(); op.setFirst(arg1); } else if (op.isType(NOT_EQUALS) && arg2.isType(VALUE) && ((ValueOp) arg2).isValue("*")) { log.debug("convert to NOT EXISTS"); op = new NotExistsOp(); op.setFirst(arg1); } } else if (!op.isType(OPEN_PAREN)) { if (stack.size() < 1) throw new SyntaxException(scanner, String.format("Missing argument for %s operator", op.getType().toSymbol())); op.setFirst(stack.pop()); } Op first = op.getFirst(); if (first == null) throw new SyntaxException(scanner, "Invalid expression"); if (first.isType(FUNCTION)) usedTags.add(first.getKeyValue()); stack.push(op); } /** * Lookup a function by its name and check that it is allowed for the kind of features that we * are reading. * * @param functionName A name to look up. */ private void saveFunction(String functionName) { StyleFunction function = FunctionFactory.createFunction(functionName); if (function == null) throw new SyntaxException(String.format("No function with name '%s()'", functionName)); // TODO: supportsWay split into supportsPoly{line,gon}, or one function supports(kind) boolean supported = false; switch (kind) { case POINT: if (function.supportsNode()) supported = true; break; case POLYLINE: if (function.supportsWay()) supported = true; break; case POLYGON: if (function.supportsWay()) supported = true; break; case RELATION: if (function.supportsRelation()) supported = true; break; case ALL: if (function.supportsNode() || function.supportsWay() || function.supportsRelation()) supported = true; break; } if (!supported) throw new SyntaxException(String.format("Function '%s()' not supported for %s", functionName, kind)); stack.push(function); } private void pushValue(String value) { stack.push(new ValueOp(value)); } }