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