001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools.template_engine;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.util.ArrayList;
007import java.util.Arrays;
008import java.util.Collection;
009import java.util.List;
010
011import org.openstreetmap.josm.data.osm.search.SearchCompiler;
012import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
013import org.openstreetmap.josm.data.osm.search.SearchParseError;
014import org.openstreetmap.josm.tools.template_engine.Tokenizer.Token;
015import org.openstreetmap.josm.tools.template_engine.Tokenizer.TokenType;
016
017/**
018 * Template parser.
019 */
020public class TemplateParser {
021    private final Tokenizer tokenizer;
022
023    private static final Collection<TokenType> EXPRESSION_END_TOKENS = Arrays.asList(TokenType.EOF);
024    private static final Collection<TokenType> CONDITION_WITH_APOSTROPHES_END_TOKENS = Arrays.asList(TokenType.APOSTROPHE);
025
026    /**
027     * Constructs a new {@code TemplateParser}.
028     * @param template template string to parse
029     */
030    public TemplateParser(String template) {
031        this.tokenizer = new Tokenizer(template);
032    }
033
034    private Token check(TokenType expectedToken) throws ParseError {
035        Token token = tokenizer.nextToken();
036        if (token.getType() != expectedToken)
037            throw new ParseError(token, expectedToken);
038        else
039            return token;
040    }
041
042    /**
043     * Parse the template.
044     * @return the resulting template entry
045     * @throws ParseError if the template cannot be parsed
046     */
047    public TemplateEntry parse() throws ParseError {
048        return parseExpression(EXPRESSION_END_TOKENS);
049    }
050
051    private TemplateEntry parseExpression(Collection<TokenType> endTokens) throws ParseError {
052        List<TemplateEntry> entries = new ArrayList<>();
053        while (true) {
054            TemplateEntry templateEntry;
055            Token token = tokenizer.lookAhead();
056            if (token.getType() == TokenType.CONDITION_START) {
057                templateEntry = parseCondition();
058            } else if (token.getType() == TokenType.CONTEXT_SWITCH_START) {
059                templateEntry = parseContextSwitch();
060            } else if (token.getType() == TokenType.VARIABLE_START) {
061                templateEntry = parseVariable();
062            } else if (endTokens.contains(token.getType()))
063                return CompoundTemplateEntry.fromArray(entries.toArray(new TemplateEntry[0]));
064            else if (token.getType() == TokenType.TEXT) {
065                tokenizer.nextToken();
066                templateEntry = new StaticText(token.getText());
067            } else
068                throw new ParseError(token);
069            entries.add(templateEntry);
070        }
071    }
072
073    private TemplateEntry parseVariable() throws ParseError {
074        check(TokenType.VARIABLE_START);
075        String variableName = check(TokenType.TEXT).getText();
076        check(TokenType.END);
077
078        return new Variable(variableName);
079    }
080
081    private void skipWhitespace() throws ParseError {
082        Token token = tokenizer.lookAhead();
083        if (token.getType() == TokenType.TEXT && token.getText().trim().isEmpty()) {
084            tokenizer.nextToken();
085        }
086    }
087
088    private TemplateEntry parseCondition() throws ParseError {
089        check(TokenType.CONDITION_START);
090        Collection<TemplateEntry> conditionEntries = new ArrayList<>();
091        while (true) {
092
093            TemplateEntry condition;
094            Token searchExpression = tokenizer.skip('\'');
095            check(TokenType.APOSTROPHE);
096            condition = parseExpression(CONDITION_WITH_APOSTROPHES_END_TOKENS);
097            check(TokenType.APOSTROPHE);
098            String searchText = searchExpression.getText().trim();
099            if (searchText.isEmpty()) {
100                conditionEntries.add(condition);
101            } else {
102                try {
103                    conditionEntries.add(new SearchExpressionCondition(
104                            SearchCompiler.compile(searchText), condition));
105                } catch (SearchParseError e) {
106                    throw new ParseError(searchExpression.getPosition(), e);
107                }
108            }
109            skipWhitespace();
110
111            Token token = tokenizer.lookAhead();
112            if (token.getType() == TokenType.END) {
113                tokenizer.nextToken();
114                return new Condition(conditionEntries);
115            } else {
116                check(TokenType.PIPE);
117            }
118        }
119    }
120
121    private TemplateEntry parseContextSwitch() throws ParseError {
122
123        check(TokenType.CONTEXT_SWITCH_START);
124        Token searchExpression = tokenizer.skip('\'');
125        check(TokenType.APOSTROPHE);
126        TemplateEntry template = parseExpression(CONDITION_WITH_APOSTROPHES_END_TOKENS);
127        check(TokenType.APOSTROPHE);
128        ContextSwitchTemplate result;
129        String searchText = searchExpression.getText().trim();
130        if (searchText.isEmpty())
131            throw new ParseError(tr("Expected search expression"));
132        else {
133            try {
134                Match match = SearchCompiler.compile(searchText);
135                result = new ContextSwitchTemplate(match, template, searchExpression.getPosition());
136            } catch (SearchParseError e) {
137                throw new ParseError(searchExpression.getPosition(), e);
138            }
139        }
140        skipWhitespace();
141        check(TokenType.END);
142        return result;
143    }
144}