
org.thymeleaf.TemplateParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of thymeleaf Show documentation
Show all versions of thymeleaf Show documentation
Modern server-side Java template engine for both web and standalone environments
/*
* =============================================================================
*
* Copyright (c) 2011, The THYMELEAF team (http://www.thymeleaf.org)
*
* 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.thymeleaf;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thymeleaf.exceptions.ConfigurationException;
import org.thymeleaf.exceptions.ParserInitializationException;
import org.thymeleaf.exceptions.ParsingException;
import org.thymeleaf.exceptions.TemplateInputException;
import org.thymeleaf.resourceresolver.IResourceResolver;
import org.thymeleaf.templateresolver.ITemplateResolver;
import org.thymeleaf.templateresolver.TemplateResolution;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXParseException;
/**
*
* @author Daniel Fernández
*
* @since 1.0
*
*/
public final class TemplateParser {
private static final Logger logger = LoggerFactory.getLogger(TemplateParser.class);
private static final String SAXPARSEEXCEPTION_BAD_ELEMENT_CONTENT = "The content of elements must consist of well-formed character data or markup.";
private static final String SAXPARSEEXCEPTION_BAD_ELEMENT_CONTENT_EXPLANATION =
"The content of elements must consist of well-formed character data or markup. A usual reason for this is that one of your elements contains " +
"unescaped special XML symbols like '<' inside its body, which is forbidden by XML rules. For example, if you have '<' inside a )", Pattern.DOTALL);
static final Pattern STYLE_CDATA_PATTERN = Pattern.compile("(\\)", Pattern.DOTALL);
static final String SCRIPT_COMMENTED_CDATA_REPLACEMENT = "$1\n//\n$3";
static final String STYLE_COMMENTED_CDATA_REPLACEMENT = "$1\n/**/\n$3";
private static final ErrorHandler ERROR_HANDLER = new ErrorHandler();
private final DocumentBuilder[] nonValidatingDocumentBuilders;
private final DocumentBuilder[] validatingDocumentBuilders;
private final boolean htmlCleanerInClasspath;
private final int maxConcurrency;
private int currentNonValidatingBuilder;
private int currentValidatingBuilder;
private final Xmlizer xmlizer;
private ParsedTemplateCache parsedTemplateCache;
TemplateParser(final Configuration configuration) {
super();
this.htmlCleanerInClasspath = computeIsHtmlCleanerInClassPath();
final int availableProcessors = Runtime.getRuntime().availableProcessors();
this.maxConcurrency = (availableProcessors <= 2? availableProcessors : availableProcessors - 1);
this.nonValidatingDocumentBuilders = new DocumentBuilder[this.maxConcurrency];
this.validatingDocumentBuilders = new DocumentBuilder[this.maxConcurrency];
if (this.htmlCleanerInClasspath) {
this.xmlizer = new Xmlizer(this.maxConcurrency);
} else {
this.xmlizer = null;
}
this.currentNonValidatingBuilder = 0;
this.currentValidatingBuilder = 0;
logger.info("[THYMELEAF] Initializing template parser with a pool of {} parser/s (Number of available processors: {})",
Integer.valueOf(this.maxConcurrency), Integer.valueOf(availableProcessors));
final DocumentBuilderFactory nonValidatingDocumentBuilderFactory = DocumentBuilderFactory.newInstance();
nonValidatingDocumentBuilderFactory.setValidating(false);
final DocumentBuilderFactory validatingDocumentBuilderFactory = DocumentBuilderFactory.newInstance();
validatingDocumentBuilderFactory.setValidating(true);
for (int i = 0; i < this.maxConcurrency; i++) {
try {
this.nonValidatingDocumentBuilders[i] = nonValidatingDocumentBuilderFactory.newDocumentBuilder();
this.nonValidatingDocumentBuilders[i].setEntityResolver(new EntityResolver(configuration));
this.nonValidatingDocumentBuilders[i].setErrorHandler(ERROR_HANDLER);
this.validatingDocumentBuilders[i] = validatingDocumentBuilderFactory.newDocumentBuilder();
this.validatingDocumentBuilders[i].setEntityResolver(new EntityResolver(configuration));
this.validatingDocumentBuilders[i].setErrorHandler(ERROR_HANDLER);
} catch (final ParserConfigurationException e) {
throw new ParserInitializationException("Error creating document builder", e);
}
}
this.parsedTemplateCache =
new ParsedTemplateCache(configuration.getParsedTemplateCacheSize());
}
void clearParsedTemplateCache() {
this.parsedTemplateCache.clearParsedTemplateCache();
}
void clearParsedTemplateCacheFor(final String templateName) {
this.parsedTemplateCache.clearParsedTemplateCacheFor(templateName);
}
private boolean computeIsHtmlCleanerInClassPath() {
try {
Thread.currentThread().getContextClassLoader().loadClass("org.htmlcleaner.HtmlCleaner");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
private int validatingRoundRobin() {
int rr;
synchronized(this) {
rr = this.currentValidatingBuilder;
this.currentValidatingBuilder = (this.currentValidatingBuilder + 1) % this.maxConcurrency;
}
return rr;
}
private int nonValidatingRoundRobin() {
int rr;
synchronized(this) {
rr = this.currentNonValidatingBuilder;
this.currentNonValidatingBuilder = (this.currentNonValidatingBuilder + 1) % this.maxConcurrency;
}
return rr;
}
public Document parseXMLString(final String xmlString) {
final String wrappedText = "" + xmlString + " ";
final InputSource inputSource = new InputSource(new StringReader(wrappedText));
final int roundRobin = nonValidatingRoundRobin();
try {
synchronized (this.nonValidatingDocumentBuilders[roundRobin]) {
return this.nonValidatingDocumentBuilders[roundRobin].parse(inputSource);
}
} catch (final IOException e) {
throw new TemplateInputException("Exception parsing document", e);
} catch (final SAXParseException e) {
if (e.getMessage() != null && e.getMessage().contains(SAXPARSEEXCEPTION_BAD_ELEMENT_CONTENT)) {
throw new ParsingException(SAXPARSEEXCEPTION_BAD_ELEMENT_CONTENT_EXPLANATION, e);
}
throw new ParsingException("An exception happened during parsing", e);
} catch (final Exception e) {
throw new ParsingException("Exception parsing document", e);
}
}
public ParsedTemplate parseDocument(final Arguments arguments) {
final String templateName = arguments.getTemplateName();
final ParsedTemplate cached =
this.parsedTemplateCache.getParsedTemplate(templateName);
if (cached != null) {
return cached.clone();
}
final Configuration configuration = arguments.getConfiguration();
final Set templateResolvers = configuration.getTemplateResolvers();
TemplateResolution templateResolution = null;
InputStream templateInputStream = null;
for (final ITemplateResolver templateResolver : templateResolvers) {
if (templateInputStream == null) {
templateResolution = templateResolver.resolveTemplate(arguments);
if (templateResolution != null) {
final String resourceName = templateResolution.getResourceName();
final IResourceResolver resourceResolver = templateResolution.getResourceResolver();
if (logger.isTraceEnabled()) {
logger.trace("[THYMELEAF][{}] Trying to resolve template \"{}\" as resource \"{}\" with resource resolver \"{}\"", new Object[] {TemplateEngine.threadIndex(), templateName, resourceName, resourceResolver.getName()});
}
templateInputStream =
resourceResolver.getResourceAsStream(arguments, resourceName);
if (templateInputStream == null) {
if (logger.isTraceEnabled()) {
logger.trace("[THYMELEAF][{}] Template \"{}\" could not be resolved as resource \"{}\" with resource resolver \"{}\"", new Object[] {TemplateEngine.threadIndex(), templateName, resourceName, resourceResolver.getName()});
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("[THYMELEAF][{}] Template \"{}\" was correctly resolved as resource \"{}\" in mode {} with resource resolver \"{}\"", new Object[] {TemplateEngine.threadIndex(), templateName, resourceName, templateResolution.getTemplateMode(), resourceResolver.getName()});
}
}
} else {
if (logger.isTraceEnabled()) {
logger.trace("[THYMELEAF][{}] Skipping template resolver \"{}\" for template \"{}\"", new Object[] {TemplateEngine.threadIndex(), templateResolver.getName(), templateName});
}
}
}
}
if (templateResolution == null || templateInputStream == null) {
throw new TemplateInputException(
"Error resolving template \"" + arguments.getTemplateName() + "\", " +
"template might not exist or might not be accessible by " +
"any of the configured Template Resolvers");
}
if (logger.isTraceEnabled()) {
logger.trace("[THYMELEAF][{}] Starting parsing of template \"{}\"", TemplateEngine.threadIndex(), templateName);
}
final Document document = doParseDocument(templateResolution, templateInputStream);
if (logger.isTraceEnabled()) {
logger.trace("[THYMELEAF][{}] Finished parsing of template \"{}\"", TemplateEngine.threadIndex(), templateName);
}
final ParsedTemplate templateDocumentResolution =
new ParsedTemplate(templateName, templateResolution, document);
if (templateResolution.getValidity().isCacheable()) {
this.parsedTemplateCache.putParsedTemplate(templateDocumentResolution);
return templateDocumentResolution.clone();
}
return templateDocumentResolution;
}
private Document doParseDocument(
final TemplateResolution templateResolution, final InputStream inputStream) {
final boolean validating =
templateResolution.getTemplateMode().isValidating();
final int roundRobin =
(validating? validatingRoundRobin() : nonValidatingRoundRobin());
final DocumentBuilder[] builders =
(validating? this.validatingDocumentBuilders : this.nonValidatingDocumentBuilders);
InputSource inputSource = null;
if (templateResolution.getTemplateMode().equals(TemplateMode.LEGACYHTML5)) {
if (logger.isTraceEnabled()) {
logger.trace("[THYMELEAF][{}] Template \"{}\" is in LEGACYHTML5 mode, converting to XML-formed document", TemplateEngine.threadIndex(), templateResolution.getTemplateName());
}
final String xml = xmlizeHTMLInputStream(templateResolution, inputStream);
inputSource = new InputSource(new StringReader(xml));
if (logger.isTraceEnabled()) {
logger.trace("[THYMELEAF][{}] Template \"{}\" converted to XML-formed document", TemplateEngine.threadIndex(), templateResolution.getTemplateName());
}
} else {
inputSource = new InputSource(inputStream);
final String encoding = templateResolution.getCharacterEncoding();
if (encoding != null && !encoding.trim().equals("")) {
inputSource.setEncoding(encoding);
}
}
try {
synchronized (builders[roundRobin]) {
return builders[roundRobin].parse(inputSource);
}
} catch (final IOException e) {
throw new TemplateInputException("Exception parsing document", e);
} catch (final SAXParseException e) {
if (e.getMessage() != null && e.getMessage().contains(SAXPARSEEXCEPTION_BAD_ELEMENT_CONTENT)) {
throw new ParsingException(SAXPARSEEXCEPTION_BAD_ELEMENT_CONTENT_EXPLANATION, e);
}
throw new ParsingException("An exception happened during parsing", e);
} catch (final Exception e) {
throw new ParsingException("Exception parsing document", e);
} finally {
try{
inputStream.close();
} catch (final Exception e) {
// Ignored
}
}
}
public String xmlizeHTMLInputStream(
final TemplateResolution templateResolution, final InputStream inputStream) {
if (!this.htmlCleanerInClasspath) {
throw new ConfigurationException(
"Cannot perform conversion to XML from legacy HTML: The HtmlCleaner library " +
"is not in the classpath. HtmlCleaner 2.2+ is required for processing templates in " +
"LEGACYHTML5 mode [http://htmlcleaner.sourceforge.net].");
}
return this.xmlizer.xmlize(templateResolution, inputStream);
}
/*
* This class is isolated so that HtmlCleaner does not have to be in the
* classpath to work if no LEGACYHTML5 templates are present
*/
private static class Xmlizer {
private int maxConcurrency;
private final org.htmlcleaner.HtmlCleaner[] htmlCleaners;
private int currentHtmlCleaner;
Xmlizer(final int maxConcurrency) {
super();
this.maxConcurrency = maxConcurrency;
this.htmlCleaners = new org.htmlcleaner.HtmlCleaner[this.maxConcurrency];
this.currentHtmlCleaner = 0;
for (int i = 0; i < this.maxConcurrency; i++) {
this.htmlCleaners[i] = new org.htmlcleaner.HtmlCleaner();
final org.htmlcleaner.CleanerProperties props = this.htmlCleaners[i].getProperties();
props.setOmitXmlDeclaration(true);
props.setOmitDoctypeDeclaration(true);
}
}
private int htmlCleanerRoundRobin() {
int rr;
synchronized(this) {
rr = this.currentHtmlCleaner;
this.currentHtmlCleaner = (this.currentHtmlCleaner + 1) % this.maxConcurrency;
}
return rr;
}
String xmlize(
final TemplateResolution templateResolution, final InputStream inputStream) {
final int roundRobin = htmlCleanerRoundRobin();
try {
final String encoding = templateResolution.getCharacterEncoding();
final org.htmlcleaner.HtmlCleaner htmlCleaner = this.htmlCleaners[roundRobin];
synchronized (htmlCleaner) {
final org.htmlcleaner.TagNode tagNode =
(encoding != null && !encoding.trim().equals("")?
htmlCleaner.clean(inputStream, encoding) : htmlCleaner.clean(inputStream));
final org.htmlcleaner.Serializer serializer =
new org.htmlcleaner.SimpleXmlSerializer(htmlCleaner.getProperties());
// No need to pass an encoding because HtmlCleaner only uses it
// for writing the XML declaration at the beginning of the document,
String xml = serializer.getAsString(tagNode);
// section start and end fragments in