/* * Copyright 2013 Martin Kouba * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.trimou.engine.parser; import static org.trimou.engine.config.EngineConfigurationKey.END_DELIMITER; import static org.trimou.engine.config.EngineConfigurationKey.START_DELIMITER; import static org.trimou.util.Checker.checkArgumentNotEmpty; import static org.trimou.util.Checker.checkArgumentsNotNull; import java.io.BufferedReader; import java.io.IOException; import java.io.Reader; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.trimou.engine.MustacheEngine; import org.trimou.engine.MustacheTagType; import org.trimou.engine.config.EngineConfigurationKey; import org.trimou.exception.MustacheException; import org.trimou.exception.MustacheProblem; import org.trimou.util.ImmutableSet; import org.trimou.util.Strings; /** * The default parser. It's not thread-safe and may not be reused. * * @author Martin Kouba */ class DefaultParser implements Parser { private static final Logger LOGGER = LoggerFactory .getLogger(DefaultParser.class); private MustacheEngine engine; private State state; private int line; private int delimiterIdx; private boolean triple; private StringBuilder buffer; private final Delimiters delimiters; private ParsingHandler handler; private final Set<String> supportedSeparators; private List<String> lastMatchedSeparators; private Set<Character> zeroIndexNonSeparatorCharacters; private int separatorIdx; /** * * @param engine */ public DefaultParser(MustacheEngine engine) { this.state = State.TEXT; this.line = 1; this.delimiterIdx = 0; this.triple = false; this.buffer = new StringBuilder(); this.separatorIdx = 0; this.engine = engine; this.delimiters = new Delimiters(engine.getConfiguration() .getStringPropertyValue(START_DELIMITER), engine .getConfiguration().getStringPropertyValue(END_DELIMITER)); this.supportedSeparators = ImmutableSet.of(Strings.LINE_SEPARATOR_LF, Strings.LINE_SEPARATOR_CR, Strings.LINE_SEPARATOR_CRLF); this.zeroIndexNonSeparatorCharacters = new HashSet<>(); } public void parse(String name, Reader reader, ParsingHandler handler) { checkArgumentNotEmpty(name); checkArgumentsNotNull(reader, handler); this.handler = handler; reader = ensureBufferedReader(reader); try { // Start of document handler.startTemplate(name, delimiters, engine); int val; while ((val = reader.read()) != -1) { processCharacter((char) val); } if (buffer.length() > 0) { if (state == State.TEXT) { // Flush the last text segment flushText(); } else { throw new MustacheException( MustacheProblem.COMPILE_INVALID_TEMPLATE, "Unexpected non-text buffer at the end of the document (probably unterminated tag): %s", buffer); } } if (state == State.LINE_SEPARATOR) { // Flush the last line separator lineSeparatorFound(lastMatchedSeparators.get(0)); } // End of document handler.endTemplate(); } catch (IOException e) { throw new MustacheException(MustacheProblem.COMPILE_IO_ERROR, e); } } private void processCharacter(char character) { switch (state) { case TEXT: text(character); break; case START_TAG: startTag(character); break; case TAG: tag(character); break; case END_TAG: endTag(character); break; case LINE_SEPARATOR: lineSeparator(character); break; default: throw new IllegalStateException("Unknown parsing state"); } } private void text(char character) { if (character == delimiters.getStart(0)) { if (delimiters.isStartOver(delimiterIdx)) { tagStartFound(); } else { // Probably multi-char start tag state = State.START_TAG; delimiterIdx = 1; } } else if ((lastMatchedSeparators = findMatchingSeparators(character, 0)) .size() > 0) { if (lastMatchedSeparators.size() == 1 && lastMatchedSeparators.get(0).length() == 1) { // Single-char separator lineSeparatorFound(lastMatchedSeparators.get(0)); } else if ((lastMatchedSeparators.size() > 1) || (lastMatchedSeparators.size() == 1 && lastMatchedSeparators .get(0).length() > 1)) { // Multiple separators or multi-char separator state = State.LINE_SEPARATOR; separatorIdx = 1; } } else { buffer.append(character); } } private void startTag(char character) { if (character == delimiters.getStart(delimiterIdx)) { if (delimiters.isStartOver(delimiterIdx)) { tagStartFound(); } else { delimiterIdx++; } } else { // False alarm - not a start delimiter state = State.TEXT; buffer.append(delimiters.getStartPart(delimiterIdx)); delimiterIdx = 0; processCharacter(character); } } private void tag(char character) { if (character == delimiters.getEnd(0)) { if (triple) { // Triple mustache detected - skip first ending // mustache buffer.append(character); triple = false; } else if (delimiters.isEndOver(delimiterIdx)) { // One char delimiter flushTag(); } else { // Ending tag state = State.END_TAG; delimiterIdx = 1; } } else { if (character == delimiters.getStart(0) && buffer.length() == 0) { // Most likely a triple mustache triple = true; } buffer.append(character); } } private void endTag(char character) { if (character == delimiters.getEnd(delimiterIdx)) { if (delimiters.isEndOver(delimiterIdx)) { flushTag(); } else { delimiterIdx++; } } else { // False alarm - not an end delimiter LOGGER.info( "Tag contains a part of the end delimiter - most probably an invalid key [part: {}, line: {}]", delimiters.getStartPart(delimiterIdx), line); state = State.TAG; buffer.append(delimiters.getEndPart(delimiterIdx)); buffer.append(character); delimiterIdx = 0; } } private void lineSeparator(char character) { List<String> matched = findMatchingSeparators(character, separatorIdx); if (matched.isEmpty()) { // Single-char separator for (String separator : lastMatchedSeparators) { if (separator.length() == separatorIdx) { lineSeparatorFound(separator); processCharacter(character); } } } else if (matched.size() == 1) { // Multi-char separator lineSeparatorFound(matched.get(0)); } else if (matched.size() > 1) { lastMatchedSeparators = matched; separatorIdx++; } } /** * Line separator end - flush. * * @param lineSeparator */ private void lineSeparatorFound(String lineSeparator) { flushText(); flushLineSeparator(lineSeparator); line++; state = State.TEXT; separatorIdx = 0; } /** * Real tag start, flush text if any. */ private void tagStartFound() { state = State.TAG; delimiterIdx = 0; flushText(); } private void flushText() { if (buffer.length() > 0) { handler.text(buffer.toString()); clearBuffer(); } } /** * Real tag end - flush. */ private void flushTag() { state = State.TEXT; handler.tag(deriveTag(buffer.toString())); delimiterIdx = 0; clearBuffer(); } private void flushLineSeparator(String separator) { handler.lineSeparator(separator); } private Reader ensureBufferedReader(Reader reader) { return reader instanceof BufferedReader ? reader : new BufferedReader( reader); } private ParsedTag deriveTag(String buffer) { MustacheTagType type = identifyTagType(buffer); String key = extractContent(type, buffer); return new ParsedTag(key, type); } /** * Identify the tag type (variable, comment, etc.). * * @param buffer * @param delimiters * @return the tag type */ private MustacheTagType identifyTagType(String buffer) { if (buffer.length() == 0) { return MustacheTagType.VARIABLE; } // Triple mustache is supported for default delimiters only if (delimiters.hasDefaultDelimitersSet() && buffer.charAt(0) == ((String) EngineConfigurationKey.START_DELIMITER .getDefaultValue()).charAt(0) && buffer.charAt(buffer.length() - 1) == ((String) EngineConfigurationKey.END_DELIMITER .getDefaultValue()).charAt(0)) { return MustacheTagType.UNESCAPE_VARIABLE; } Character command = buffer.charAt(0); for (MustacheTagType type : MustacheTagType.values()) { if (command.equals(type.getCommand())) { return type; } } return MustacheTagType.VARIABLE; } /** * Extract the tag content. * * @param buffer * @return */ private String extractContent(MustacheTagType tagType, String buffer) { switch (tagType) { case VARIABLE: return buffer.trim(); case UNESCAPE_VARIABLE: return (buffer.charAt(0) == ((String) EngineConfigurationKey.START_DELIMITER .getDefaultValue()).charAt(0) ? buffer.substring(1, buffer.length() - 1).trim() : buffer.substring(1).trim()); case SECTION: case INVERTED_SECTION: case PARTIAL: case EXTEND: case EXTEND_SECTION: case SECTION_END: case NESTED_TEMPLATE: case COMMENT: return buffer.substring(1).trim(); case DELIMITER: return buffer.trim(); default: return null; } } private void clearBuffer() { this.buffer = new StringBuilder(); } /** * * @param character * @param atIndex * @return the list of matching line separators */ private List<String> findMatchingSeparators(char character, int atIndex) { if (atIndex == 0 && zeroIndexNonSeparatorCharacters.contains(character)) { return Collections.emptyList(); } List<String> matchedSeparators = new ArrayList<>( supportedSeparators.size()); for (String separator : supportedSeparators) { if (separator.length() > atIndex && separator.charAt(atIndex) == character) { matchedSeparators.add(separator); } } if (atIndex == 0 && matchedSeparators.isEmpty()) { zeroIndexNonSeparatorCharacters.add(character); } return matchedSeparators; } private enum State { TEXT, START_TAG, TAG, END_TAG, LINE_SEPARATOR; } }