/* * 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.REMOVE_STANDALONE_LINES; import static org.trimou.engine.config.EngineConfigurationKey.REMOVE_UNNECESSARY_SEGMENTS; import static org.trimou.engine.config.EngineConfigurationKey.REUSE_LINE_SEPARATOR_SEGMENTS; import static org.trimou.exception.MustacheProblem.COMPILE_INVALID_TAG; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.regex.Matcher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.trimou.Mustache; import org.trimou.engine.MustacheEngine; import org.trimou.engine.MustacheTagType; import org.trimou.engine.config.EngineConfigurationKey; import org.trimou.engine.segment.CommentSegment; import org.trimou.engine.segment.ContainerSegment; import org.trimou.engine.segment.ExtendSectionSegment; import org.trimou.engine.segment.ExtendSegment; import org.trimou.engine.segment.InvertedSectionSegment; import org.trimou.engine.segment.LineSeparatorSegment; import org.trimou.engine.segment.Origin; import org.trimou.engine.segment.PartialSegment; import org.trimou.engine.segment.RootSegment; import org.trimou.engine.segment.SectionSegment; import org.trimou.engine.segment.Segment; import org.trimou.engine.segment.SegmentType; import org.trimou.engine.segment.SetDelimitersSegment; import org.trimou.engine.segment.TextSegment; import org.trimou.engine.segment.ValueSegment; import org.trimou.exception.MustacheException; import org.trimou.exception.MustacheProblem; import org.trimou.util.ImmutableList; import org.trimou.util.ImmutableList.ImmutableListBuilder; import org.trimou.util.Patterns; import org.trimou.util.Strings; /** * The default handler implementation that compiles the template. It's not * thread-safe and must not be reused. * * @author Martin Kouba */ class DefaultParsingHandler implements ParsingHandler { private static final Logger LOGGER = LoggerFactory .getLogger(DefaultParsingHandler.class); private final Deque<ContainerSegmentBase> containerStack = new ArrayDeque<>(); private final List<Template> nestedTemplates = new ArrayList<>(); private MustacheEngine engine; private String templateName; private Delimiters delimiters; private Template template; private long start; private int line = 1; private int index = 0; private boolean skipValueEscaping; private boolean handlebarsSupportEnabled; private NestedTemplateBase currentNestedBase; @Override public void startTemplate(String name, Delimiters delimiters, MustacheEngine engine) { this.delimiters = delimiters; this.engine = engine; this.templateName = name; containerStack.addFirst(new RootSegmentBase()); skipValueEscaping = engine.getConfiguration().getBooleanPropertyValue( EngineConfigurationKey.SKIP_VALUE_ESCAPING); handlebarsSupportEnabled = engine.getConfiguration() .getBooleanPropertyValue( EngineConfigurationKey.HANDLEBARS_SUPPORT_ENABLED); start = System.currentTimeMillis(); LOGGER.debug("Start compilation of {}", new Object[] { name }); } @Override public void endTemplate() { RootSegmentBase rootSegmentBase = validate(); // Post processing if (engine.getConfiguration() .getBooleanPropertyValue(REMOVE_STANDALONE_LINES)) { SegmentBases.removeStandaloneLines(rootSegmentBase); } if (engine.getConfiguration() .getBooleanPropertyValue(REMOVE_UNNECESSARY_SEGMENTS)) { SegmentBases.removeUnnecessarySegments(rootSegmentBase); } if (engine.getConfiguration() .getBooleanPropertyValue(REUSE_LINE_SEPARATOR_SEGMENTS)) { SegmentBases.reuseLineSeparatorSegments(rootSegmentBase); } template = new Template(engine.getConfiguration() .getIdentifierGenerator().generate(Mustache.class), templateName, engine, nestedTemplates); template.initRootSegment(rootSegmentBase.asSegment(template)); for (Template nested : nestedTemplates) { nested.initParent(template); } LOGGER.debug("Compilation of {} finished [time: {} ms, segments: {}]", templateName, System.currentTimeMillis() - start, template.getRootSegment().getSegmentsSize(true)); nestedTemplates.clear(); containerStack.clear(); } @Override public void text(String text) { addSegment(new SegmentBase(SegmentType.TEXT, text, line, incrementAndGetIndex())); } @Override public void tag(ParsedTag tag) { validateTag(tag); switch (tag.getType()) { case COMMENT: addSegment(new SegmentBase(tag, line, incrementAndGetIndex())); break; case UNESCAPE_VARIABLE: case VARIABLE: valueSegment(tag); break; case PARTIAL: addSegment( new PartialSegmentBase(tag, line, incrementAndGetIndex())); break; case DELIMITER: changeDelimiters(tag.getContent()); addSegment(new SegmentBase(tag, line, incrementAndGetIndex())); break; case SECTION: case INVERTED_SECTION: case EXTEND: case EXTEND_SECTION: push(new ContainerSegmentBase(tag, line, incrementAndGetIndex())); break; case NESTED_TEMPLATE: nestedTemplate(tag); break; case SECTION_END: endSection(tag.getContent()); break; default: throw new IllegalStateException("Unsupported tag type"); } } @Override public void lineSeparator(String separator) { addSegment( new LineSeparatorBase(separator, line, incrementAndGetIndex())); line++; } public Mustache getCompiledTemplate() { if (template == null) { throw new MustacheException(MustacheProblem.TEMPLATE_NOT_READY); } return template; } private void validateTag(ParsedTag tag) { if (Strings.isEmpty(tag.getContent())) { throw new MustacheException(COMPILE_INVALID_TAG, "Tag has no content [type: %s, line: %s]", tag.getType(), line); } if (tag.getContent().contains(delimiters.getStart())) { throw new MustacheException(COMPILE_INVALID_TAG, "Tag content contains current start delimiter [type: %s, line: %s, delimiter: %s]", tag.getType(), line, delimiters.getStart()); } if (handlebarsSupportEnabled) { // Only validate segments which cannot have a HelperExecutionHandler // associated if (MustacheTagType.contentMustBeValidated(tag.getType()) && !MustacheTagType.supportsHelpers(tag.getType()) && Strings.containsWhitespace(tag.getContent())) { contentMustBeNonWhitespaceSequenceException(tag.getType()); } } else { if (MustacheTagType .contentMustBeNonWhitespaceCharacterSequence(tag.getType()) && Strings.containsWhitespace(tag.getContent())) { contentMustBeNonWhitespaceSequenceException(tag.getType()); } } } private MustacheException contentMustBeNonWhitespaceSequenceException( MustacheTagType tagType) { throw new MustacheException(COMPILE_INVALID_TAG, "Tag content must be a non-whitespace character sequence [template: %s, type: %s, line: %s]", templateName, tagType, line); } private void endSection(String key) { ContainerSegmentBase container = pop(); if (container == null || SegmentType.ROOT.equals(container.getType()) || (!handlebarsSupportEnabled && !key.equals(container.getContent())) || (handlebarsSupportEnabled && !container.getContent().startsWith(key)) || (handlebarsSupportEnabled && !container.getContent().contains(" ") && !key.equals(container.getContent()))) { // a) No container on the stack // b) Handlebars support not enabled and section start key does not // equal to section end key // c) Handlebars support enabled and section start key does not // start with section end key StringBuilder msg = new StringBuilder(); List<String> params = new ArrayList<>(); msg.append("Invalid section end: "); if (container == null || SegmentType.ROOT.equals(container.getType())) { msg.append("%s has no matching section start"); params.add(key); } else { msg.append("%s is not matching section start %s"); params.add(key); params.add(container.getContent()); } msg.append(" [line: %s]"); params.add("" + line); throw new MustacheException( MustacheProblem.COMPILE_INVALID_SECTION_END, msg.toString(), params.toArray()); } if (container instanceof NestedTemplateBase) { // Do not add nested template as a segment NestedTemplateBase nestedBase = (NestedTemplateBase) container; Template nested = new Template(engine.getConfiguration() .getIdentifierGenerator().generate(Mustache.class), container.getContent(), engine); nested.initRootSegment(nestedBase.asSegment(nested)); nestedTemplates.add(nested); currentNestedBase = null; } else { addSegment(container); } } /** * E.g. =<% %>=, =[ ]= * * @param key */ private void changeDelimiters(String key) { if (key.charAt(0) != MustacheTagType.DELIMITER.getCommand() || key.charAt(key.length() - 1) != MustacheTagType.DELIMITER .getCommand()) { throw new MustacheException( MustacheProblem.COMPILE_INVALID_DELIMITERS, "Invalid set delimiters tag: %s [line: %s]", key, line); } Matcher matcher = Patterns.newSetDelimitersContentPattern() .matcher(key.substring(1, key.length() - 1)); if (matcher.find()) { delimiters.setNewValues(matcher.group(1), matcher.group(3)); } else { throw new MustacheException( MustacheProblem.COMPILE_INVALID_DELIMITERS, "Invalid delimiters set: %s [line: %s]", key, line); } } /** * Push the container wrapper on the stack. * * @param container */ private void push(ContainerSegmentBase container) { containerStack.addFirst(container); LOGGER.trace("Push {} [name: {}]", container.getType(), container.getContent()); } /** * * @return the container wrapper removed from the stack */ private ContainerSegmentBase pop() { ContainerSegmentBase container = containerStack.removeFirst(); LOGGER.trace("Pop {} [name: {}]", container.getType(), container.getContent()); return container; } /** * Add the segment to the container on the stack. * * @param segment */ private void addSegment(SegmentBase segment) { containerStack.peekFirst().addSegment(segment); LOGGER.trace("Add {}", segment); } /** * Validate the compiled template. */ private RootSegmentBase validate() { ContainerSegmentBase root = containerStack.peekFirst(); if (!(root instanceof RootSegmentBase)) { throw new MustacheException( MustacheProblem.COMPILE_INVALID_TEMPLATE, "Incorrect last container segment on the stack: %s", containerStack.peekFirst().toString(), line); } return (RootSegmentBase) root; } private int incrementAndGetIndex() { return ++index; } private void valueSegment(ParsedTag tag) { addSegment(new ValueSegmentBase(tag, line, incrementAndGetIndex(), skipValueEscaping)); } private void nestedTemplate(ParsedTag tag) { if (engine.getConfiguration().getBooleanPropertyValue( EngineConfigurationKey.NESTED_TEMPLATE_SUPPORT_ENABLED)) { // First check existing nested templates for (Template nested : nestedTemplates) { if (nested.getName().equals(tag.getContent())) { throw new MustacheException( MustacheProblem.COMPILE_NESTED_TEMPLATE_ERROR, "A nested template with the name [%s] is already defined at line %s in the template [%s]", tag.getContent(), // The root segment of a nested template references // the line from the defining template nested.getRootSegment().getOrigin().getLine(), templateName); } } NestedTemplateBase nestedBase = new NestedTemplateBase( tag.getContent(), line, index); if (currentNestedBase != null) { throw new MustacheException( MustacheProblem.COMPILE_NESTED_TEMPLATE_ERROR, "Nested templates within nested template definitions are not supported: %s", containerStack.peekFirst().toString()); } else { currentNestedBase = nestedBase; } push(nestedBase); } else { valueSegment( new ParsedTag( MustacheTagType.NESTED_TEMPLATE.getCommand() + tag.getContent(), MustacheTagType.VARIABLE)); } } /** * Root segment */ static class RootSegmentBase extends ContainerSegmentBase { RootSegmentBase() { super(SegmentType.ROOT, null, 0, 0); } @Override public RootSegment asSegment(Template template) { return new RootSegment(new Origin(template), getSegments(template)); } } static class NestedTemplateBase extends ContainerSegmentBase { NestedTemplateBase(String content, int line, int index) { super(null, content, line, index); } @Override public RootSegment asSegment(Template template) { return new RootSegment(new Origin(template, getLine(), getIndex()), getSegments(template)); } } static class ContainerSegmentBase extends SegmentBase implements Iterable<SegmentBase> { private final List<SegmentBase> segments; ContainerSegmentBase(SegmentType type, String content, int line, int index) { super(type, content, line, index); this.segments = new ArrayList<>(); } ContainerSegmentBase(ParsedTag tag, int line, int index) { super(tag, line, index); this.segments = new ArrayList<>(); } boolean addSegment(SegmentBase segment) { if (SegmentType.EXTEND.equals(getType()) && !SegmentType.EXTEND_SECTION.equals(segment.getType())) { // Only add extending sections return false; } return segments.add(segment); } @Override ContainerSegment asSegment(Template template) { switch (getType()) { case SECTION: return new SectionSegment(getContent(), getOrigin(template), getSegments(template)); case INVERTED_SECTION: return new InvertedSectionSegment(getContent(), getOrigin(template), getSegments(template)); case EXTEND: return new ExtendSegment(getContent(), getOrigin(template), getSegments(template)); case EXTEND_SECTION: return new ExtendSectionSegment(getContent(), getOrigin(template), getSegments(template)); default: throw new IllegalStateException( "Invalid tag type: " + getType()); } } protected List<Segment> getSegments(Template template) { ImmutableListBuilder<Segment> builder = ImmutableList.builder(); for (SegmentBase wrapper : segments) { builder.add(wrapper.asSegment(template)); } return builder.build(); } @Override public Iterator<SegmentBase> iterator() { return segments.iterator(); } ListIterator<SegmentBase> listIterator() { return segments.listIterator(); } } static class LineSeparatorBase extends SegmentBase { // Cache the segment so that reuse is possible private LineSeparatorSegment segment; public LineSeparatorBase(String content, int line, int index) { super(SegmentType.LINE_SEPARATOR, content, line, index); } @Override Segment asSegment(Template template) { if (segment == null) { segment = new LineSeparatorSegment(getContent(), getOrigin(template)); } return segment; } } static class ValueSegmentBase extends SegmentBase { private boolean unescape; ValueSegmentBase(ParsedTag tag, int line, int index, boolean skipValueEscaping) { super(SegmentType.VALUE, tag.getContent(), line, index); unescape = skipValueEscaping || tag.getType().equals(MustacheTagType.UNESCAPE_VARIABLE); } @Override ValueSegment asSegment(Template template) { return new ValueSegment(getContent(), getOrigin(template), unescape); } } static class PartialSegmentBase extends SegmentBase { private String indentation; PartialSegmentBase(ParsedTag tag, int line, int index) { super(tag, line, index); } public void setIndentation(String indentation) { this.indentation = indentation; } @Override public PartialSegment asSegment(Template template) { return new PartialSegment(getContent(), getOrigin(template), indentation); } } static class SegmentBase { private final SegmentType type; private final String content; private final int line; private final int index; SegmentBase(ParsedTag tag, int line, int index) { this.content = tag.getContent(); this.type = SegmentType.fromTag(tag.getType()); this.line = line; this.index = index; } SegmentBase(SegmentType type, String content, int line, int index) { this.type = type; this.content = content; this.line = line; this.index = index; } SegmentType getType() { return type; } String getContent() { return content; } int getLine() { return line; } int getIndex() { return index; } Segment asSegment(Template template) { switch (type) { case TEXT: return new TextSegment(content, getOrigin(template)); case COMMENT: return new CommentSegment(content, getOrigin(template)); case DELIMITERS: return new SetDelimitersSegment(content, getOrigin(template)); default: throw new IllegalStateException( "Unsupported segment type: " + type); } } protected Origin getOrigin(Template template) { return new Origin(template, line, index); } @Override public String toString() { return String.format("[type: %s, content: %s, line: %s, idx: %s]", type, content, line, index); } } }