| /* |
| * Copyright 2009 Google Inc. |
| * |
| * 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 com.google.gwt.safehtml.rebind; |
| |
| import com.google.gwt.core.ext.TreeLogger; |
| import com.google.gwt.core.ext.UnableToCompleteException; |
| import com.google.gwt.dev.util.Preconditions; |
| import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.HtmlContext; |
| import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.ParameterChunk; |
| import com.google.gwt.safehtml.shared.SafeHtmlUtils; |
| |
| import org.xml.sax.Attributes; |
| import org.xml.sax.InputSource; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.SAXNotSupportedException; |
| import org.xml.sax.SAXParseException; |
| import org.xml.sax.XMLReader; |
| import org.xml.sax.helpers.DefaultHandler; |
| import org.xml.sax.helpers.XMLReaderFactory; |
| |
| import java.io.IOException; |
| import java.io.Reader; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * A HTML context-aware parser for a simple HTML template language. |
| * |
| * <p>This parser parses templates consisting of well-formed XML or XHTML |
| * markup, with template parameters of the form {@code "{n}"}. For example, a |
| * template might look like, |
| * <pre> {@code |
| * <span class="{0}"><a href="{1}/{2}">{3}</a></span> |
| * }</pre> |
| * |
| * <p>The parser produces a parsed form of the template (returned as a |
| * {@link ParsedHtmlTemplate}) consisting of a sequence of chunks |
| * corresponding to the literal strings and parameters of the template. The |
| * parser is HTML context aware and tags each parameter with its parameter index |
| * as well as a {@link HtmlContext} that corresponds to the HTML context in |
| * which the parameter occurs in the template. |
| * |
| * <p>The following contexts are recognized and instantiated: |
| * <dl> |
| * <dt>{@link HtmlContext.Type#TEXT} |
| * <dd>This context corresponds to basic inner text. In the above example, |
| * parameter #3 would be tagged with this context. |
| * <dt>{@link HtmlContext.Type#ATTRIBUTE_START} |
| * <dd>This context corresponds to a parameter that appears at the very start |
| * of a HTML attribute's value; in the above example this applies to |
| * parameters #0 and #1. |
| * <dt>{@link HtmlContext.Type#ATTRIBUTE} |
| * <dd>This context corresponds to a parameter that appears within an |
| * attribute in a position other than at the start of the attribute's |
| * value. In the above example, this applies to parameter #2. |
| * </dl> |
| * |
| * <p>For both attribute contexts, the {@code tag} and {@code attribute} |
| * properties of the context are set to the name of the enclosing tag and |
| * attribute, respectively. |
| * |
| * <p>Tag and attribute names are converted to lower-case in {@link HtmlContext} |
| * properties and literal string chunks of the parsed form. |
| * |
| * <p>The implementation is subject to the following limitations: |
| * <ul> |
| * <li>The input template must be well-formed XML/XHTML. If it is not, |
| * a {@link UnableToCompleteException} is thrown and details regarding |
| * the source of the parse failure are logged to this parser's logger. |
| * <li>Template parameters can only appear within inner text and within |
| * attributes. In particular, parameters cannot appear within a HTML |
| * tag or attribute name; for example, the following is not a valid |
| * template: |
| * <pre> {@code |
| * <span><{0} class="xyz" {1}="..."/></span> |
| * }</pre> |
| * <li>The output markup will contain separate closing tags for tags without |
| * content. I.e., an input template |
| * <pre> {@code |
| * <img src="..."/> |
| * }</pre> |
| * will result in the corresponding output |
| * <pre> {@code |
| * <img src="..."></img> |
| * }</pre> |
| * <li>There is no escaping mechanism for the parameter syntax, i.e. it is |
| * impossible to write a template that results in a literal output chunk |
| * containing a substring of the form "{@code {0}}". |
| * </ul> |
| */ |
| final class HtmlTemplateParser { |
| |
| /** |
| * A SAX parser event handler for parsing HTML templates. |
| */ |
| private class HtmlTemplateHandler extends DefaultHandler { |
| |
| /* |
| * Overrides for relevant SAX event handler methods. |
| */ |
| |
| @Override |
| public void characters(char[] ch, int start, int length) { |
| parseTemplateString( |
| new HtmlContext(HtmlContext.Type.TEXT), |
| SafeHtmlUtils.htmlEscape(new String(ch, start, length))); |
| } |
| |
| @Override |
| public void endElement(String uri, String localName, String name) { |
| Preconditions.checkArgument(uri.equals(""), |
| "Namespace uri unexpectedly non-empty: %s", uri); |
| appendLiteral("</" + name.toLowerCase() + ">"); |
| } |
| |
| @Override |
| public void endPrefixMapping(String prefix) throws SAXException { |
| throw unsupportedError("Prefix Mapping"); |
| } |
| |
| @Override |
| public void error(SAXParseException e) { |
| getLogger().log(TreeLogger.ERROR, "Parser error: " + e); |
| } |
| |
| @Override |
| public void fatalError(SAXParseException e) throws SAXException { |
| getLogger().log(TreeLogger.ERROR, "Parser fatal error: " + e); |
| throw e; |
| } |
| |
| /* |
| * Throw errors on various irrelevant SAX events that we don't want to |
| * handle, and which should not occur in templates. |
| * |
| * It may be reasonable to just silently ignore these events, but failing |
| * explicitly seems more helpful to developers. |
| */ |
| |
| @Override |
| public void notationDecl(String name, String publicId, String systemId) |
| throws SAXException { |
| throw unsupportedError("Notation Declaration"); |
| } |
| |
| @Override |
| public void processingInstruction(String target, String data) |
| throws SAXException { |
| throw unsupportedError("Processing Instruction"); |
| } |
| |
| @Override |
| public InputSource resolveEntity(String publicId, String systemId) |
| throws SAXException { |
| throw unsupportedError("External Entity"); |
| } |
| |
| @Override |
| public void skippedEntity(String name) throws SAXException { |
| throw unsupportedError("Skipped Entity"); |
| } |
| |
| @Override |
| public void startElement(String uri, String localName, String name, |
| Attributes attributes) { |
| Preconditions.checkArgument(uri.equals(""), |
| "Namespace uri unexpectedly non-empty: %s", uri); |
| |
| name = name.toLowerCase(); |
| appendLiteral("<" + name); |
| if (attributes != null) { |
| int len = attributes.getLength(); |
| for (int i = 0; i < len; i++) { |
| String attribute = attributes.getQName(i).toLowerCase(); |
| appendLiteral(" " + attribute + "=\""); |
| parseTemplateString( |
| new HtmlContext(HtmlContext.Type.ATTRIBUTE, name, attribute), |
| new HtmlContext(HtmlContext.Type.ATTRIBUTE_START, |
| name, attribute), |
| SafeHtmlUtils.htmlEscape(attributes.getValue(i))); |
| appendLiteral("\""); |
| } |
| } |
| appendLiteral(">"); |
| } |
| |
| /* |
| * Error handlers. |
| */ |
| |
| @Override |
| public void startPrefixMapping(String prefix, String uri) |
| throws SAXException { |
| throw unsupportedError("Prefix Mapping"); |
| } |
| |
| @Override |
| public void unparsedEntityDecl(String name, String publicId, |
| String systemId, String notationName) throws SAXException { |
| throw unsupportedError("Unparsed Entity Declaration"); |
| } |
| |
| @Override |
| public void warning(SAXParseException e) { |
| getLogger().log(TreeLogger.WARN, "Parser warning: " + e); |
| } |
| |
| /** |
| * Returns exception for unsupported event in SafeHtmlTemplates. |
| * |
| * <p> |
| * Returns an exception indicating that the event in question is not |
| * supported in SafeHtmlTemplates. |
| * |
| * @param what unsupported SAX event that should not occur in templates |
| * @return exception stating that the event is not allowed |
| */ |
| private SAXNotSupportedException unsupportedError(String what) { |
| return new SAXNotSupportedException( |
| "Not allowed in SafeHtmlTemplates: " + what); |
| } |
| } |
| |
| /** |
| * Pattern to find template parameters references. |
| */ |
| private static final Pattern TEMPLATE_PARAM_PATTERN = |
| Pattern.compile("\\{(\\d+)\\}"); |
| |
| private final TreeLogger logger; |
| |
| private final ParsedHtmlTemplate parsedTemplate; |
| |
| /** |
| * Creates a {@link HtmlTemplateParser}. |
| * |
| * @param logger the {@link TreeLogger} to log to |
| */ |
| public HtmlTemplateParser(TreeLogger logger) { |
| this.logger = logger; |
| this.parsedTemplate = new ParsedHtmlTemplate(); |
| } |
| |
| /** |
| * Returns this parser's logger. |
| */ |
| public TreeLogger getLogger() { |
| return logger; |
| } |
| |
| /** |
| * Returns the parsed representation of the template. |
| */ |
| public ParsedHtmlTemplate getParsedTemplate() { |
| return parsedTemplate; |
| } |
| |
| /** |
| * Parses a XML/XHTML document. |
| * |
| * @param input a {@link Reader} from which the document to be parsed will be |
| * read. |
| * @throws UnableToCompleteException if the template cannot be parsed. Details |
| * on the source of the failure will have been logged to this parser's |
| * logger. |
| */ |
| public void parseXHtml(Reader input) throws UnableToCompleteException { |
| HtmlTemplateHandler saxEventHandler = new HtmlTemplateHandler(); |
| XMLReader xmlParser; |
| try { |
| xmlParser = XMLReaderFactory.createXMLReader(); |
| } catch (SAXException e) { |
| logger.log(TreeLogger.ERROR, "Couldn't instantiate XML parser", e); |
| throw new UnableToCompleteException(); |
| } |
| |
| xmlParser.setContentHandler(saxEventHandler); |
| xmlParser.setDTDHandler(saxEventHandler); |
| xmlParser.setEntityResolver(saxEventHandler); |
| xmlParser.setErrorHandler(saxEventHandler); |
| |
| try { |
| xmlParser.parse(new InputSource(input)); |
| } catch (IOException e) { |
| logger.log(TreeLogger.ERROR, "Error during template parsing:", e); |
| throw new UnableToCompleteException(); |
| } catch (SAXParseException e) { |
| String logMessage = "Parse Error during template parsing, at line " |
| + e.getLineNumber() + ", column " + e.getColumnNumber(); |
| // Attempt to extract (some) of the input to provide a more useful |
| // error message. |
| try { |
| input.reset(); |
| char[] buf = new char[200]; |
| int len = input.read(buf); |
| if (len > 0) { |
| logMessage += " of input " + new String(buf, 0, len); |
| } |
| } catch (IOException e1) { |
| // We tried, but resetting/reading from the input stream failed. Sorry. |
| logMessage += " <failed to read input snippet>"; |
| } |
| logger.log(TreeLogger.ERROR, logMessage + ": " + e); |
| throw new UnableToCompleteException(); |
| } catch (SAXException e) { |
| logger.log(TreeLogger.ERROR, "Error during template parsing:", e); |
| throw new UnableToCompleteException(); |
| } |
| } |
| |
| /** |
| * Parses a {@link String} that may contain template parameters of the form |
| * {@code {n}} into corresponding literal and parameter |
| * {@link ParsedHtmlTemplate.TemplateChunk}s. |
| * |
| * <p>Parameters will be tagged with the {@link HtmlContext} provided. |
| * |
| * <p>If {@code contextAtStart} is not {@code null} and the parsed template |
| * starts with a parameter (i.e., is of the form {@code "{n}..."}), this first |
| * parameter will be tagged with that context; any other parameters will |
| * be tagged with context {@code context}. |
| * |
| * @param context the context with which to tag parameters occurring in the |
| * template |
| * @param contextAtStart if not {@code null}, the context with which to tag a |
| * parameter that occurs at the very beginning of the template |
| * @param template the template {@link String} to parse |
| */ |
| // @VisibleForTesting |
| void parseTemplateString(HtmlContext context, |
| HtmlContext contextAtStart, String template) { |
| Matcher match = TEMPLATE_PARAM_PATTERN.matcher(template); |
| |
| boolean firstMatch = true; |
| int endOfLastMatch = 0; |
| while (match.find()) { |
| if (match.start() > endOfLastMatch) { |
| // There is a non-empty string between the previous match and this |
| // match; add this as a literal chunk to the parsed representation. |
| appendLiteral(template.substring(endOfLastMatch, match.start())); |
| } |
| |
| int paramIndex = Integer.parseInt(match.group(1)); |
| if (firstMatch && (match.start() == 0) && (contextAtStart != null)) { |
| parsedTemplate.addParameter(new ParameterChunk(contextAtStart, |
| paramIndex)); |
| } else { |
| parsedTemplate.addParameter(new ParameterChunk(context, paramIndex)); |
| } |
| |
| firstMatch = false; |
| endOfLastMatch = match.end(); |
| } |
| |
| // Add a literal chunk for the substring after the last match, if any. |
| if (endOfLastMatch < template.length()) { |
| parsedTemplate.addLiteral(template.substring(endOfLastMatch)); |
| } |
| } |
| |
| /** |
| * Parses a {@link String} that may contain template parameters of the form |
| * {@code {n}} into corresponding literal and parameter |
| * {@link ParsedHtmlTemplate.TemplateChunk}s. |
| * |
| * <p>Parameters will be tagged with the {@link HtmlContext} provided. |
| * |
| * @param context the context with which to tag parameters occurring in the |
| * template |
| * @param template the template to parse |
| */ |
| // @VisibleForTesting |
| void parseTemplateString(HtmlContext context, String template) { |
| parseTemplateString(context, null, template); |
| } |
| |
| /** |
| * Appends a literal string to the parsed template representation. |
| * |
| * <p>The {@code literal} will be appended without processing; any XML/XHTML |
| * markup as well as template parameters occurring in the {@code literal} will |
| * not be parsed. |
| * |
| * @param literal the string to append |
| */ |
| private void appendLiteral(String literal) { |
| parsedTemplate.addLiteral(literal); |
| } |
| } |