SafeHtml Templates The SafeHtml Templates were removed from the main SafeHtml review, but are being added again here so that widgets can start using this api, even while a newer parser is in the works. This is a first pass at templates, and the api/usage are up for review. Review at http://gwt-code-reviews.appspot.com/793801 git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@8657 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/safehtml/SafeHtml.gwt.xml b/user/src/com/google/gwt/safehtml/SafeHtml.gwt.xml index 9721d70..5c2d658 100644 --- a/user/src/com/google/gwt/safehtml/SafeHtml.gwt.xml +++ b/user/src/com/google/gwt/safehtml/SafeHtml.gwt.xml
@@ -20,4 +20,7 @@ <inherits name='com.google.gwt.user.User'/> <source path="client"/> <source path="shared"/> + <generate-with class="com.google.gwt.safehtml.rebind.SafeHtmlTemplatesGenerator"> + <when-type-assignable class="com.google.gwt.safehtml.client.SafeHtmlTemplates"/> + </generate-with> </module>
diff --git a/user/src/com/google/gwt/safehtml/client/SafeHtmlTemplates.java b/user/src/com/google/gwt/safehtml/client/SafeHtmlTemplates.java new file mode 100644 index 0000000..858d0b1 --- /dev/null +++ b/user/src/com/google/gwt/safehtml/client/SafeHtmlTemplates.java
@@ -0,0 +1,65 @@ +/* + * Copyright 2010 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.client; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A tag interface that facilitates compile-time binding of HTML templates to + * generate SafeHtml strings. + * + * <p>Example usage: + * <pre> + * public interface MyTemplate extends SafeHtmlTemplates { + * @Template("<span class=\"{3}\">{0}: <a href=\"{1}\">{2}</a></span>") + * SafeHtml messageWithLink(SafeHtml message, String url, String linkText, + * String style); + * } + * private static final MyTemplate TEMPLATE = GWT.create(MyTemplate.class); + * public void useTemplate(...) { + * SafeHtml message; + * String url; + * String linkText; + * String style; + * // ... + * SafeHtml messageWithLink = + * TEMPLATE.messageWithLink(message, url, linkText, style); + * } + * </pre> + * + * Instantiating a SafeHtmlTemplates interface with GWT.create() returns an + * instance of an implementation that is generated at compile time. The code + * generator parses the value of each template method's @Template annotation as + * a (X)HTML template, with template variables denoted by curly-brace + * placeholders that refer by index to the corresponding template method + * parameter. + */ +public interface SafeHtmlTemplates { + + /** + * The HTML template. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @Documented + public @interface Template { + String value(); + } +}
diff --git a/user/src/com/google/gwt/safehtml/rebind/HtmlTemplateParser.java b/user/src/com/google/gwt/safehtml/rebind/HtmlTemplateParser.java new file mode 100644 index 0000000..8db7e1e --- /dev/null +++ b/user/src/com/google/gwt/safehtml/rebind/HtmlTemplateParser.java
@@ -0,0 +1,402 @@ +/* + * 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); + } +}
diff --git a/user/src/com/google/gwt/safehtml/rebind/ParsedHtmlTemplate.java b/user/src/com/google/gwt/safehtml/rebind/ParsedHtmlTemplate.java new file mode 100644 index 0000000..15ec091 --- /dev/null +++ b/user/src/com/google/gwt/safehtml/rebind/ParsedHtmlTemplate.java
@@ -0,0 +1,283 @@ +/* + * 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.dev.util.Preconditions; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + * A representation of a parsed (X)HTML template. + * + * <p>A parsed template is represented as a sequence of template chunks. + * + * <p>A chunk may correspond to a literal string or to a formal template + * parameter. + * + * <p>Parameter chunks have an attribute that refers to the index of the + * corresponding template method parameter, as well as an attribute that + * represents the HTML context the template parameter appeared in. + */ +final class ParsedHtmlTemplate { + + /** + * A representation of the context of a point within a HTML document. A + * context consists of a type (such as "plain text", "attribute"), as well as + * a HTML tag and attribute name where applicable. + */ + static final class HtmlContext { + /** + * The possible types of HTML context. + */ + // TODO(xtof): Possibly add support for other contexts, such as style. + static enum Type { + /** + * The undefined HTML context. Contexts of this type are meant to be used + * in cases where a parser is used that is not HTML context-aware. + */ + UNDEFINED, + /** + * Regular inner text HTML context. + */ + TEXT, + /** + * HTML attribute context. + */ + ATTRIBUTE, + /** + * At the very start of an attribute. + */ + ATTRIBUTE_START, + } + + private final Type type; + private final String tag; + private final String attribute; + + /** + * Creates a HTML context. + * + * @param type the {@link Type} of this context + */ + public HtmlContext(Type type) { + this(type, null, null); + } + + /** + * Creates a HTML context. + * + * @param type the {@link Type} of this context + * @param tag the HTML tag this context corresponds to, if applicable; null + * otherwise + * @param attribute the HTML attribute this context corresponds to, if + * applicable; null otherwise + */ + public HtmlContext(Type type, String tag, String attribute) { + Preconditions.checkArgument((type != Type.UNDEFINED) + || ((tag == null) && (attribute == null)), + "tag and attribute must be null for context \"UNDEFINED\""); + Preconditions.checkArgument((type != Type.TEXT) + || ((tag == null) && (attribute == null)), + "tag and attribute must be null for context \"TEXT\""); + this.type = type; + this.tag = tag; + this.attribute = attribute; + } + + /** + * Returns the attribute of this HTML context. + */ + public String getAttribute() { + return attribute; + } + + /** + * Returns the tag of this HTML context. + */ + public String getTag() { + return tag; + } + + /** + * Returns the type of this HTML context. + */ + public Type getType() { + return type; + } + + @Override + public String toString() { + return "(" + getType() + "," + getTag() + "," + getAttribute() + ")"; + } + } + + /** + * Represents a template chunk corresponding to a literal string. + */ + static final class LiteralChunk implements TemplateChunk { + private final StringBuilder chunk; + + /** + * Creates a literal chunk corresponding to an empty string. + */ + public LiteralChunk() { + chunk = new StringBuilder(); + } + + /** + * Creates a literal chunk corresponding to the provided string. + */ + public LiteralChunk(String literal) { + chunk = new StringBuilder(literal); + } + + public Kind getKind() { + return Kind.LITERAL; + } + + public String getLiteral() { + return chunk.toString(); + } + + @Override + public String toString() { + return String.format("L(%s)", getLiteral()); + } + + void append(String s) { + chunk.append(s); + } + } + + /** + * Represents a template chunk corresponding to a template parameter. + */ + static final class ParameterChunk implements TemplateChunk { + + private final HtmlContext context; + private final int parameterIndex; + + /** + * Creates a parameter chunk. + * + * @param context the HTML context this parameter appears in + * @param parameterIndex the index of the template parameter that this chunk + * refers to + */ + public ParameterChunk(HtmlContext context, int parameterIndex) { + this.context = context; + this.parameterIndex = parameterIndex; + } + + /** + * Returns the HTML context this parameter appears in. + */ + public HtmlContext getContext() { + return context; + } + + public Kind getKind() { + return Kind.PARAMETER; + } + + /** + * Returns the index of the template parameter that this chunk refers to. + */ + public int getParameterIndex() { + return parameterIndex; + } + + @Override + public String toString() { + return String.format("P(%s,%d)", getContext(), getParameterIndex()); + } + } + + /** + * Represents a parsed chunk of a template. + * + * <p>There are two kinds of chunks: Those representing literal strings and + * those representing template parameters. + */ + interface TemplateChunk { + /** + * Distinguishes different kinds of {@link TemplateChunk}. + */ + public static enum Kind { + LITERAL, PARAMETER, + } + + /** + * Returns the {@link Kind} of this template chunk. + */ + Kind getKind(); + } + + /* + * The chunks of this parsed template. + */ + private final LinkedList<TemplateChunk> chunks; + + /** + * Initializes an empty parsed HTML template. + */ + public ParsedHtmlTemplate() { + chunks = new LinkedList<TemplateChunk>(); + } + + /** + * Adds a literal string to the template. + * + * If the currently last chunk of the parsed template is a literal chunk, the + * provided string literal will be appended to that chunk. I.e., consecutive + * literal chunks are automatically coalesced. + + * @param literal the string to be added as a literal chunk + */ + public void addLiteral(String literal) { + if (chunks.isEmpty() + || (chunks.getLast().getKind() != TemplateChunk.Kind.LITERAL)) { + chunks.add(new LiteralChunk(literal)); + } else { + ((LiteralChunk) chunks.getLast()).append(literal); + } + } + + /** + * Adds a parameter chunk to the template. + * + * @param chunk the chunk to be added + */ + public void addParameter(ParameterChunk chunk) { + chunks.add(chunk); + } + + /** + * Returns the chunks of this parsed template. + * + * <p>The returned list is unmodifiable. + */ + public List<TemplateChunk> getChunks() { + return Collections.unmodifiableList(chunks); + } + + @Override + public String toString() { + return chunks.toString(); + } +}
diff --git a/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesGenerator.java b/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesGenerator.java new file mode 100644 index 0000000..1f0ec8a --- /dev/null +++ b/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesGenerator.java
@@ -0,0 +1,69 @@ +/* + * 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.Generator; +import com.google.gwt.core.ext.GeneratorContext; +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.UnableToCompleteException; +import com.google.gwt.core.ext.typeinfo.JClassType; +import com.google.gwt.core.ext.typeinfo.NotFoundException; +import com.google.gwt.core.ext.typeinfo.TypeOracle; +import com.google.gwt.user.rebind.ClassSourceFileComposerFactory; +import com.google.gwt.user.rebind.SourceWriter; + +import java.io.PrintWriter; + +/** + * Generator for implementations of + * {@link com.google.gwt.safehtml.client.SafeHtmlTemplates}. + */ +public class SafeHtmlTemplatesGenerator extends Generator { + + @Override + public String generate(TreeLogger logger, GeneratorContext genCtx, + String fqInterfaceName) throws UnableToCompleteException { + TypeOracle oracle = genCtx.getTypeOracle(); + JClassType interfaceType; + try { + interfaceType = oracle.getType(fqInterfaceName); + } catch (NotFoundException e) { + logger.log(TreeLogger.ERROR, "Unexpected error: " + e.getMessage(), e); + throw new UnableToCompleteException(); + } + + String implName = interfaceType.getName(); + implName = implName.replace('.', '_') + "Impl"; + + String packageName = interfaceType.getPackage().getName(); + PrintWriter printWriter = genCtx.tryCreate(logger, packageName, implName); + + if (printWriter != null) { + ClassSourceFileComposerFactory factory = + new ClassSourceFileComposerFactory(packageName, implName); + factory.addImplementedInterface(interfaceType.getQualifiedSourceName()); + SourceWriter sourceWriter = + factory.createSourceWriter(genCtx, printWriter); + + SafeHtmlTemplatesImplCreator implCreator = + new SafeHtmlTemplatesImplCreator(sourceWriter, interfaceType); + implCreator.emitClass(logger, null); + + genCtx.commit(logger, printWriter); + } + return packageName + "." + implName; + } +}
diff --git a/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplCreator.java b/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplCreator.java new file mode 100644 index 0000000..2c52990 --- /dev/null +++ b/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplCreator.java
@@ -0,0 +1,45 @@ +/* + * 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.core.ext.typeinfo.JClassType; +import com.google.gwt.core.ext.typeinfo.JMethod; +import com.google.gwt.i18n.shared.GwtLocale; +import com.google.gwt.user.rebind.AbstractGeneratorClassCreator; +import com.google.gwt.user.rebind.SourceWriter; + +/** + * Code generator for implementations of + * {@link com.google.gwt.safehtml.client.SafeHtmlTemplates}. + */ +public class SafeHtmlTemplatesImplCreator + extends AbstractGeneratorClassCreator { + + public SafeHtmlTemplatesImplCreator(SourceWriter sourceWriter, + JClassType interfaceType) { + super(sourceWriter, interfaceType); + } + + @Override + protected void emitMethodBody(TreeLogger logger, JMethod method, + GwtLocale locale) throws UnableToCompleteException { + SafeHtmlTemplatesImplMethodCreator methodCreator = + new SafeHtmlTemplatesImplMethodCreator(this); + methodCreator.createMethodFor(logger, method, null, null, locale); + } +}
diff --git a/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplMethodCreator.java b/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplMethodCreator.java new file mode 100644 index 0000000..efaea94 --- /dev/null +++ b/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplMethodCreator.java
@@ -0,0 +1,346 @@ +/* + * 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.core.ext.typeinfo.JMethod; +import com.google.gwt.core.ext.typeinfo.JParameter; +import com.google.gwt.core.ext.typeinfo.JType; +import com.google.gwt.i18n.rebind.AbstractResource.ResourceList; +import com.google.gwt.i18n.shared.GwtLocale; +import com.google.gwt.safehtml.client.SafeHtmlTemplates.Template; +import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.HtmlContext; +import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.LiteralChunk; +import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.ParameterChunk; +import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.TemplateChunk; +import com.google.gwt.safehtml.shared.SafeHtmlUtils; +import com.google.gwt.safehtml.shared.OnlyToBeUsedInGeneratedCodeStringBlessedAsSafeHtml; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.safehtml.shared.UriUtils; +import com.google.gwt.user.rebind.AbstractGeneratorClassCreator; +import com.google.gwt.user.rebind.AbstractMethodCreator; + +import java.io.StringReader; +import java.util.Arrays; +import java.util.Set; +import java.util.TreeSet; + +/** + * Method body code generator for implementations of + * {@link com.google.gwt.safehtml.client.SafeHtmlTemplates}. + */ +public class SafeHtmlTemplatesImplMethodCreator extends AbstractMethodCreator { + + /** + * Fully-qualified class name of the {@link String} class. + */ + private static final String JAVA_LANG_STRING_FQCN = String.class.getName(); + + /** + * Fully-qualified class name of the {@link SafeHtml} interface. + */ + public static final String SAFE_HTML_FQCN = SafeHtml.class.getName(); + + /** + * Fully-qualified class name of the StringBlessedAsSafeHtml class. + */ + private static final String BLESSED_STRING_FQCN = + OnlyToBeUsedInGeneratedCodeStringBlessedAsSafeHtml.class.getName(); + + /** + * Fully-qualified class name of the {@link SafeHtmlUtils} class. + */ + private static final String ESCAPE_UTILS_FQCN = SafeHtmlUtils.class.getName(); + + /** + * Fully-qualified class name of the {@link UriUtils} class. + */ + private static final String URI_UTILS_FQCN = UriUtils.class.getName(); + + /** + * The set of the names of the HTML attributes whose values are interpreted + * as URIs. + * + * <p>See <a href="http://www.w3.org/TR/html4/index/attributes.html">Index + * of Attributes</a> for reference. + */ + private static final Set<String> URI_VALUED_ATTRIBUTES; + + static { + URI_VALUED_ATTRIBUTES = new TreeSet(); + URI_VALUED_ATTRIBUTES.addAll(Arrays.asList( + "action", + "archive", + "background", + "cite", + "classid", + "codebase", + "data", + "dynsrc", + "href", + "longdesc", + "src", + "usemap")); + } + + public SafeHtmlTemplatesImplMethodCreator( + AbstractGeneratorClassCreator classCreator) { + super(classCreator); + } + + /** + * {@inheritDoc} + */ + @Override + public void createMethodFor(TreeLogger logger, JMethod targetMethod, + String key, ResourceList resourceList, GwtLocale locale) + throws UnableToCompleteException { + if (!targetMethod.getReturnType().getQualifiedSourceName().equals( + SafeHtmlTemplatesImplMethodCreator.SAFE_HTML_FQCN)) { + throw error(logger, "All methods in interfaces extending " + + "SafeHtmlTemplates must have a return type of " + + SafeHtmlTemplatesImplMethodCreator.SAFE_HTML_FQCN + "."); + } + Template templateAnnotation = targetMethod.getAnnotation(Template.class); + if (templateAnnotation == null) { + throw error(logger, "Required annotation @Template not present " + + "on interface method " + targetMethod.toString()); + } + + String template = templateAnnotation.value(); + JParameter[] params = targetMethod.getParameters(); + emitMethodBodyFromTemplate(logger, template, params); + } + + /** + * Emits an expression corresponding to a template variable in "attribute" + * context. + * + * <p>The expression emitted applies appropriate escaping and/or sanitization + * to the parameter's value depending the Java type of the corresponding + * template method parameter: + * + * <ul> + * <li>If the parameter is not of type {@link String}, it is first converted + * to {@link String}. + * <li>If the template parameter occurs at the start of a URI-valued + * attribute within the template, it is sanitized to ensure that it + * is safe in this context. This is done by passing the value through + * {@link UriUtils#sanitizeUri(String)}. + * <li>The result is then HTML-escaped by passing it through + * {@link SafeHtmlUtils#htmlEscape(String)}. + * </ul> + * + * <i>Note</i>: Template method parameters of type {@link SafeHtml} are + * <i>not</i> treated specially in an attribute context, and will be HTML- + * escaped like regular strings. This is because {@link SafeHtml} values can + * contain non-escaped HTML markup, which is not valid within attributes. + * + * @param logger the logger to log failures to + * @param htmlContext the HTML context in which the corresponding template + * variable occurs in + * @param formalParameterName the name of the template method's formal + * parameter corresponding to the expression being emitted + * @param parameterType the Java type of the corresponding template method's + * parameter + */ + private void emitAttributeContextParameterExpression(TreeLogger logger, + HtmlContext htmlContext, String formalParameterName, + JType parameterType) { + + // TODO(xtof): check attr name against a set of attributes we know we can + // handle (i.e., excluding things like onFoo, style, etc). + + /* + * Build up the expression from the "inside out", i.e. start with the formal + * parameter, convert to string if necessary, then wrap in validators if + * necessary, and finally HTML-escape if necessary. + */ + String expression = formalParameterName; + + // The parameter's value must be explicitly converted to String unless it + // is already of that type. + if (!JAVA_LANG_STRING_FQCN.equals(parameterType.getQualifiedSourceName())) { + expression = "String.valueOf(" + expression + ")"; + } + + boolean isParameterAtStartOfUriAttribute = + htmlContext.getType() == HtmlContext.Type.ATTRIBUTE_START + && URI_VALUED_ATTRIBUTES.contains(htmlContext.getAttribute()); + if (isParameterAtStartOfUriAttribute) { + expression = URI_UTILS_FQCN + ".sanitizeUri(" + expression + ")"; + } + + // TODO(xtof): Handle EscapedString subtype of SafeHtml, once it's been + // introduced. + // TODO(xtof): Throw an exception if using SafeHtml within an attribute. + expression = ESCAPE_UTILS_FQCN + ".htmlEscape(" + expression + ")"; + + print(expression); + } + + /** + * Generates code that renders the provided HTML template into an instance + * of the {@link SafeHtml} type. + * + * <p>The template is parsed as a (X)HTML template (see + * {@link HtmlTemplateParser}). From the template's parsed form, code is + * generated that, when executed, will emit an instantiation of the template. + * The generated code appropriately escapes and/or sanitizes template + * parameters such that evaluating the emitted string as HTML in a browser + * will not result in script execution. + * + * <p>As such, strings emitted from generated template methods satisfy the + * type contract of the {@link SafeHtml} type, and can therefore be returned + * wrapped as {@link SafeHtml}. + * + * @param logger the logger to log failures to + * @param template the (X)HTML template to generate code for + * @param params the parameters of the corresponding template method + * @throws UnableToCompleteException if an error occurred that prevented + * code generation for the template + */ + private void emitMethodBodyFromTemplate(TreeLogger logger, String template, + JParameter[] params) throws UnableToCompleteException { + println("StringBuilder sb = new java.lang.StringBuilder()"); + indent(); + indent(); + + HtmlTemplateParser parser = new HtmlTemplateParser(logger); + parser.parseXHtml(new StringReader(template)); + + for (TemplateChunk chunk : parser.getParsedTemplate().getChunks()) { + if (chunk.getKind() == TemplateChunk.Kind.LITERAL) { + emitStringLiteral(((LiteralChunk) chunk).getLiteral()); + } else if (chunk.getKind() == TemplateChunk.Kind.PARAMETER) { + ParameterChunk parameterChunk = (ParameterChunk) chunk; + + int formalParameterIndex = parameterChunk.getParameterIndex(); + if (formalParameterIndex < 0 || formalParameterIndex >= params.length) { + throw error(logger, "Argument " + formalParameterIndex + + " beyond range of arguments: " + template); + } + String formalParameterName = "arg" + formalParameterIndex; + JType paramType = params[formalParameterIndex].getType(); + + emitParameterExpression(logger, parameterChunk.getContext(), + formalParameterName, paramType); + } else { + throw error(logger, "Unexpected chunk kind in parsed template " + + template); + } + } + println(";"); + outdent(); + outdent(); + println("return new " + BLESSED_STRING_FQCN + "(sb.toString());"); + } + + /** + * Emits an expression corresponding to a template parameter. + * + * <p>The expression emitted applies appropriate escaping/sanitization to the + * parameter's value, depending on the parameter's HTML context, and the Java + * type of the corresponding template method parameter. + * + * @param logger the logger to log failures to + * @param htmlContext the HTML context in which the corresponding template + * variable occurs in + * @param formalParameterName the name of the template method's formal + * parameter corresponding to the expression being emitted + * @param parameterType the Java type of the corresponding template method's + * parameter + */ + private void emitParameterExpression(TreeLogger logger, + HtmlContext htmlContext, String formalParameterName, + JType parameterType) { + print(".append("); + if (htmlContext.getType() == HtmlContext.Type.TEXT) { + emitTextContextParameterExpression(formalParameterName, parameterType); + } else if (htmlContext.getType() == HtmlContext.Type.ATTRIBUTE + || htmlContext.getType() == HtmlContext.Type.ATTRIBUTE_START) { + emitAttributeContextParameterExpression(logger, htmlContext, + formalParameterName, parameterType); + } else { + throw new IllegalStateException( + "unknown HTML context for formal template parameter " + + formalParameterName + ": " + htmlContext); + } + println(")"); + } + + /** + * Emits a string literal. + * + * @param str the {@link String} to emit as a literal + */ + private void emitStringLiteral(String str) { + print(".append("); + print(wrap(str)); + println(")"); + } + + /** + * Emits an expression corresponding to a template variable in "inner text" + * context. + * + * <p>The expression emitted applies appropriate escaping to the parameter's + * value depending the Java type of the corresponding template method + * parameter: + * + * <ul> + * <li>If the parameter is of a primitive (e.g., numeric, boolean) type, or + * of type {@link SafeHtml}, it is emitted as is, without escaping. + * <li>Otherwise, an expression that passes the paramter's value through + * {@link EscapeUtils#htmlEscape(String)} is emitted. + * </ul> + * + * @param formalParameterName the name of the template method's formal + * parameter corresponding to the expression being emitted + * @param parameterType the Java type of the corresponding template method's + * parameter + */ + private void emitTextContextParameterExpression(String formalParameterName, + JType parameterType) { + boolean parameterIsPrimitiveType = (parameterType.isPrimitive() != null); + boolean parameterIsNotStringTyped = !(JAVA_LANG_STRING_FQCN.equals( + parameterType.getQualifiedSourceName())); + + if (SAFE_HTML_FQCN.equals(parameterType.getQualifiedSourceName())) { + // The parameter is of type SafeHtml and its wrapped string can + // therefore be emitted safely without escaping. + print(formalParameterName + ".asString()"); + } else if (parameterIsPrimitiveType) { + // The string representations of primitive types never contain HTML + // special characters and can therefore be emitted without escaping. + print(formalParameterName); + } else { + // The parameter is of some other type, and its value must be HTML + // escaped. Furthermore, unless the parameter's type is {@link String}, + // it must be explicitly converted to {@link String}. + print(ESCAPE_UTILS_FQCN + ".htmlEscape("); + if (parameterIsNotStringTyped) { + print("String.valueOf("); + } + print(formalParameterName); + if (parameterIsNotStringTyped) { + print(")"); + } + print(")"); + } + } +}
diff --git a/user/test/com/google/gwt/safehtml/SafeHtmlGwtSuite.java b/user/test/com/google/gwt/safehtml/SafeHtmlGwtSuite.java index 1b656e3..4d6e104 100644 --- a/user/test/com/google/gwt/safehtml/SafeHtmlGwtSuite.java +++ b/user/test/com/google/gwt/safehtml/SafeHtmlGwtSuite.java
@@ -16,6 +16,7 @@ package com.google.gwt.safehtml; import com.google.gwt.junit.tools.GWTTestSuite; +import com.google.gwt.safehtml.client.SafeHtmlTemplatesTest; import com.google.gwt.safehtml.shared.GwtSafeHtmlUtilsTest; import com.google.gwt.safehtml.shared.SafeHtmlStringTest; @@ -31,6 +32,7 @@ suite.addTestSuite(GwtSafeHtmlUtilsTest.class); suite.addTestSuite(SafeHtmlStringTest.class); + suite.addTestSuite(SafeHtmlTemplatesTest.class); return suite; }
diff --git a/user/test/com/google/gwt/safehtml/SafeHtmlJreSuite.java b/user/test/com/google/gwt/safehtml/SafeHtmlJreSuite.java index 3bf970e..e5bb101 100644 --- a/user/test/com/google/gwt/safehtml/SafeHtmlJreSuite.java +++ b/user/test/com/google/gwt/safehtml/SafeHtmlJreSuite.java
@@ -15,6 +15,8 @@ */ package com.google.gwt.safehtml; +import com.google.gwt.safehtml.rebind.HtmlTemplateParserTest; +import com.google.gwt.safehtml.rebind.ParsedHtmlTemplateTest; import com.google.gwt.safehtml.server.UriUtilsTest; import com.google.gwt.safehtml.shared.SafeHtmlUtilsTest; import com.google.gwt.safehtml.shared.SafeHtmlBuilderTest; @@ -37,6 +39,8 @@ suite.addTestSuite(SimpleHtmlSanitizerTest.class); suite.addTestSuite(SafeHtmlStringTest.class); suite.addTestSuite(UriUtilsTest.class); + suite.addTestSuite(HtmlTemplateParserTest.class); + suite.addTestSuite(ParsedHtmlTemplateTest.class); return suite; }
diff --git a/user/test/com/google/gwt/safehtml/client/SafeHtmlTemplatesTest.java b/user/test/com/google/gwt/safehtml/client/SafeHtmlTemplatesTest.java new file mode 100644 index 0000000..fbb3e2b --- /dev/null +++ b/user/test/com/google/gwt/safehtml/client/SafeHtmlTemplatesTest.java
@@ -0,0 +1,110 @@ +/* + * 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.client; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.safehtml.shared.SafeHtmlUtils; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.junit.client.GWTTestCase; + +import junit.framework.Assert; + +/** + * Tests the SafeHtmlTemplates compile-time binding mechanism. + */ +public class SafeHtmlTemplatesTest extends GWTTestCase { + + private static final String HTML_MARKUP = "woo <i>whee</i>"; + + private static final String GOOD_URL_ESCAPED = + "http://foo.com/foo<bar>&baz=dootz"; + private static final String GOOD_URL = "http://foo.com/foo<bar>&baz=dootz"; + private static final String BAD_URL = "javascript:evil(1<2)"; + private static final String BAD_URL_ESCAPED = "javascript:evil(1<2)"; + + private TestTemplates templates; + + @Override + public String getModuleName() { + return "com.google.gwt.safehtml.SafeHtml"; + } + + @Override + protected void gwtSetUp() throws Exception { + templates = GWT.create(TestTemplates.class); + } + + /** + * A SafeHtmlTemplates interface for testing. + */ + public interface TestTemplates extends SafeHtmlTemplates { + @Template("<span><b>{0}</b><span>{1}</span></span>") + SafeHtml simpleTemplate(String foo, SafeHtml bar); + + @Template("<span><a href=\"{0}\"><b>{1}</b></a></span>") + SafeHtml templateWithUriAttribute(String url, SafeHtml html); + + @Template("<div id=\"{0}\">{1}</div>") + SafeHtml templateWithRegularAttribute(String id, SafeHtml html); + + @Template("<span><img src=\"{0}/{1}\"/></span>") + SafeHtml templateWithTwoPartUriAttribute(String baseUrl, String urlPart); + } + + public void testSimpleTemplate() { + Assert.assertEquals( + "<span><b>foo<bar</b><span>woo <i>whee</i></span></span>", + templates.simpleTemplate("foo<bar", + SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString()); + } + + public void testTemplateWithUriAttribute() { + Assert.assertEquals( + "<span><a href=\"" + GOOD_URL_ESCAPED + "\"><b>" + HTML_MARKUP + + "</b></a></span>", + templates.templateWithUriAttribute( + GOOD_URL, SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString()); + Assert.assertEquals( + "<span><a href=\"#\"><b>" + HTML_MARKUP + "</b></a></span>", + templates.templateWithUriAttribute( + BAD_URL, SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString()); + } + + public void testTemplateWithRegularAttribute() { + Assert.assertEquals( + "<div id=\"" + GOOD_URL_ESCAPED + "\">" + HTML_MARKUP + "</div>", + templates.templateWithRegularAttribute( + GOOD_URL, SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString()); + + // Values inserted into non-URI-valued attributes should not be sanitized, + // but still escaped. + Assert.assertEquals( + "<div id=\"" + BAD_URL_ESCAPED + "\">" + HTML_MARKUP + "</div>", + templates.templateWithRegularAttribute( + BAD_URL, SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString()); + } + + public void testTemplateWithTwoPartUriAttribute() { + Assert.assertEquals( + "<span><img src=\"" + GOOD_URL_ESCAPED + "/x&y\"></img></span>", + templates.templateWithTwoPartUriAttribute( + GOOD_URL, "x&y").asString()); + Assert.assertEquals( + "<span><img src=\"#/x&y\"></img></span>", + templates.templateWithTwoPartUriAttribute( + BAD_URL, "x&y").asString()); + } +}
diff --git a/user/test/com/google/gwt/safehtml/rebind/HtmlTemplateParserTest.java b/user/test/com/google/gwt/safehtml/rebind/HtmlTemplateParserTest.java new file mode 100644 index 0000000..bf6f092 --- /dev/null +++ b/user/test/com/google/gwt/safehtml/rebind/HtmlTemplateParserTest.java
@@ -0,0 +1,184 @@ +/* + * 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.safehtml.rebind.ParsedHtmlTemplate.HtmlContext; +import com.google.gwt.dev.util.log.PrintWriterTreeLogger; + +import junit.framework.TestCase; + +import java.io.StringReader; + +/** + * Tests for {@link HtmlTemplateParser}. + */ +//TODO(xtof): unit tests for parse failures +public final class HtmlTemplateParserTest extends TestCase { + + private TreeLogger logger; + + @Override + public void setUp() { + logger = new PrintWriterTreeLogger(); + } + + /* + * We use the string representation of ParsedHtmlTemplate to express + * expected results. The unit test for ParsedHtmlTemplate establishes that + * this string representation accurately represents the structure of the + * parsed template. + */ + private void assertParseTemplateStringResult(String expected, + String template) { + HtmlTemplateParser parser = new HtmlTemplateParser(logger); + HtmlContext ctx = new HtmlContext(HtmlContext.Type.UNDEFINED); + parser.parseTemplateString(ctx, template); + assertEquals(expected, parser.getParsedTemplate().toString()); + } + + public void testParseTemplateString() { + assertParseTemplateStringResult("[]", ""); + assertParseTemplateStringResult("[L(foo)]", "foo"); + assertParseTemplateStringResult( + "[L(foo), P((UNDEFINED,null,null),0), L(bar)]", + "foo{0}bar"); + assertParseTemplateStringResult( + "[L(foo), P((UNDEFINED,null,null),0), " + + "P((UNDEFINED,null,null),1), L(bar)]", + "foo{0}{1}bar"); + assertParseTemplateStringResult( + "[L(foo), P((UNDEFINED,null,null),0), L(b), " + + "P((UNDEFINED,null,null),1), L(bar)]", + "foo{0}b{1}bar"); + assertParseTemplateStringResult( + "[L(foo), P((UNDEFINED,null,null),0), L(baz), " + + "P((UNDEFINED,null,null),1), L(bar)]", + "foo{0}baz{1}bar"); + assertParseTemplateStringResult( + "[P((UNDEFINED,null,null),0), L(foo), P((UNDEFINED,null,null),0), " + + "P((UNDEFINED,null,null),1), L(bar)]", + "{0}foo{0}{1}bar"); + assertParseTemplateStringResult( + "[P((UNDEFINED,null,null),0), L(foo), P((UNDEFINED,null,null),0), " + + "P((UNDEFINED,null,null),1), L(b)]", + "{0}foo{0}{1}b"); + assertParseTemplateStringResult( + "[L(foo), P((UNDEFINED,null,null),0), P((UNDEFINED,null,null),1), " + + "L(bar), P((UNDEFINED,null,null),2)]", + "foo{0}{1}bar{2}"); + assertParseTemplateStringResult( + "[L(f), P((UNDEFINED,null,null),2), P((UNDEFINED,null,null),1), " + + "L(bar), P((UNDEFINED,null,null),2)]", + "f{2}{1}bar{2}"); + + // Test degenerate cases with curly braces that don't match a parameter + // pattern; these are treated as regular string literals. + assertParseTemplateStringResult("[L(foo{)]", "foo{"); + assertParseTemplateStringResult("[L(}foo)]", "}foo"); + assertParseTemplateStringResult("[L(foo{text})]", "foo{text}"); + } + + private void assertParseTemplateStringResultWithAtStartContext( + String expected, String template) { + HtmlTemplateParser parser = new HtmlTemplateParser(logger); + HtmlContext ctx = new HtmlContext(HtmlContext.Type.ATTRIBUTE, "img", "src"); + HtmlContext ctxAtStart = + new HtmlContext(HtmlContext.Type.ATTRIBUTE_START, "img", "src"); + parser.parseTemplateString(ctx, ctxAtStart, template); + assertEquals(expected, parser.getParsedTemplate().toString()); + } + + public void testParseTemplateStringWithAtStartContext() { + assertParseTemplateStringResultWithAtStartContext( + "[P((ATTRIBUTE_START,img,src),0)]", + "{0}"); + assertParseTemplateStringResultWithAtStartContext( + "[L(x), P((ATTRIBUTE,img,src),0)]", + "x{0}"); + assertParseTemplateStringResultWithAtStartContext( + "[L(boo), P((ATTRIBUTE,img,src),0)]", + "boo{0}"); + + assertParseTemplateStringResultWithAtStartContext( + "[P((ATTRIBUTE_START,img,src),0), L(foo)]", + "{0}foo"); + assertParseTemplateStringResultWithAtStartContext( + "[P((ATTRIBUTE_START,img,src),0), P((ATTRIBUTE,img,src),1)]", + "{0}{1}"); + assertParseTemplateStringResultWithAtStartContext( + "[P((ATTRIBUTE_START,img,src),0), L(X), P((ATTRIBUTE,img,src),1)]", + "{0}X{1}"); + assertParseTemplateStringResultWithAtStartContext( + "[L(X), P((ATTRIBUTE,img,src),0), P((ATTRIBUTE,img,src),1)]", + "X{0}{1}"); + } + + private void assertParseXHtmlResult(String expected, String template) + throws UnableToCompleteException { + HtmlTemplateParser parser = new HtmlTemplateParser(logger); + parser.parseXHtml(new StringReader(template)); + assertEquals(expected, parser.getParsedTemplate().toString()); + } + + public void testParseXHtml() throws UnableToCompleteException { + // Basic cases. + assertParseXHtmlResult("[L(<b>foo</b>)]", "<b>foo</b>"); + assertParseXHtmlResult( + "[L(<span>foo<b>), P((TEXT,null,null),0), L(</b></span>)]", + "<span>foo<b>{0}</b></span>"); + assertParseXHtmlResult( + "[L(<span>foo<b>), P((TEXT,null,null),1), L(</b>), " + + "P((TEXT,null,null),0), L(</span>)]", + "<span>foo<b>{1}</b>{0}</span>"); + + // Check that tags and attributes are lower-cased, but inner text is not. + assertParseXHtmlResult("[L(<b id=\"bAr\">fOo</b>)]", + "<B Id=\"bAr\">fOo</B>"); + + // Verify correct handling/escaping of HTML metacharacters and + // CDATA sections. + assertParseXHtmlResult( + "[L(<span>foo&bar<b>), P((TEXT,null,null),1), " + + "L(</b>foo-cdata <baz>), P((TEXT,null,null),0), " + + "L(</span>)]", + "<span>foo&bar<b>{1}</b><![CDATA[foo-cdata <baz>]]>{0}</span>"); + + // Check correct handling of ATTRIBUTE vs ATTRIBUTE_START context. + assertParseXHtmlResult( + "[L(<a href=\"), P((ATTRIBUTE_START,a,href),0), " + + "L(\">), P((TEXT,null,null),1), L(</a>)]", + "<a href=\"{0}\">{1}</a>"); + assertParseXHtmlResult( + "[L(<a href=\"http://), P((ATTRIBUTE,a,href),0), " + + "L(\">), P((TEXT,null,null),1), L(</a>)]", + "<a href=\"http://{0}\">{1}</a>"); + assertParseXHtmlResult( + "[L(<a href=\"), P((ATTRIBUTE_START,a,href),0), " + + "L(/), P((ATTRIBUTE,a,href),1), " + + "L(\">), P((TEXT,null,null),2), L(</a>)]", + "<a href=\"{0}/{1}\">{2}</a>"); + + // Verify correct escaping in attributes. + assertParseXHtmlResult( + "[L(<a href=\"http://...&), " + + "P((ATTRIBUTE,a,href),0), " + + "L(=), P((ATTRIBUTE,a,href),1), " + + "L(\">), P((TEXT,null,null),2), L(</a>)]", + "<a href=\"http://...&{0}={1}\">{2}</a>"); + } +}
diff --git a/user/test/com/google/gwt/safehtml/rebind/ParsedHtmlTemplateTest.java b/user/test/com/google/gwt/safehtml/rebind/ParsedHtmlTemplateTest.java new file mode 100644 index 0000000..ef05cfb --- /dev/null +++ b/user/test/com/google/gwt/safehtml/rebind/ParsedHtmlTemplateTest.java
@@ -0,0 +1,159 @@ +/* + * 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.safehtml.rebind.ParsedHtmlTemplate.HtmlContext; +import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.LiteralChunk; +import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.ParameterChunk; +import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.TemplateChunk; + +import junit.framework.TestCase; + +import java.util.Iterator; +import java.util.List; + +/** + * Tests for {@link ParsedHtmlTemplate}. + * + */ +public final class ParsedHtmlTemplateTest extends TestCase { + public void testAddLiteral() { + ParsedHtmlTemplate parsed = new ParsedHtmlTemplate(); + parsed.addLiteral("<foo>"); + + List<TemplateChunk> chunks = parsed.getChunks(); + LiteralChunk chunk = (LiteralChunk) chunks.get(0); + assertEquals(TemplateChunk.Kind.LITERAL, chunk.getKind()); + assertEquals("<foo>", chunk.getLiteral()); + assertEquals("L(<foo>)", chunk.toString()); + + assertEquals("[L(<foo>)]", parsed.toString()); + } + + public void testAddParameter() { + ParsedHtmlTemplate parsed = new ParsedHtmlTemplate(); + parsed.addParameter(new ParameterChunk(new HtmlContext( + HtmlContext.Type.TEXT), 0)); + + List<TemplateChunk> chunks = parsed.getChunks(); + + ParameterChunk chunk = (ParameterChunk) chunks.get(0); + assertEquals(TemplateChunk.Kind.PARAMETER, chunk.getKind()); + assertEquals(HtmlContext.Type.TEXT, chunk.getContext().getType()); + assertNull(chunk.getContext().getTag()); + assertNull(chunk.getContext().getAttribute()); + assertEquals(0, chunk.getParameterIndex()); + assertEquals("P((TEXT,null,null),0)", chunk.toString()); + + assertEquals("[P((TEXT,null,null),0)]", parsed.toString()); + } + + /** + * Tests that calling addLiteral(), addParameter(), addLiteral() in sequence + * results in the expected ParsedHtmlTemplate. + */ + public void testAddLiteralAddParameterSequence() { + ParsedHtmlTemplate parsed = new ParsedHtmlTemplate(); + + parsed.addLiteral("<foo>"); + parsed.addParameter(new ParameterChunk(new HtmlContext( + HtmlContext.Type.TEXT), 0)); + parsed.addLiteral("</foo>"); + + List<TemplateChunk> chunks = parsed.getChunks(); + assertEquals(3, chunks.size()); + Iterator<TemplateChunk> it = chunks.iterator(); + + LiteralChunk litChunk; + ParameterChunk paramChunk; + + litChunk = (LiteralChunk) it.next(); + assertEquals(TemplateChunk.Kind.LITERAL, litChunk.getKind()); + assertEquals("<foo>", litChunk.getLiteral()); + assertEquals("L(<foo>)", litChunk.toString()); + + paramChunk = (ParameterChunk) it.next(); + assertEquals(TemplateChunk.Kind.PARAMETER, paramChunk.getKind()); + assertEquals(HtmlContext.Type.TEXT, paramChunk.getContext().getType()); + assertNull(paramChunk.getContext().getTag()); + assertNull(paramChunk.getContext().getAttribute()); + assertEquals(0, paramChunk.getParameterIndex()); + assertEquals("P((TEXT,null,null),0)", paramChunk.toString()); + + litChunk = (LiteralChunk) it.next(); + assertEquals(TemplateChunk.Kind.LITERAL, litChunk.getKind()); + assertEquals("</foo>", litChunk.getLiteral()); + assertEquals("L(</foo>)", litChunk.toString()); + + // Assert that the string representation of the parsed template has the + // expected format, to allow us to use the string representation in unit + // tests for the template parser. + assertEquals("[L(<foo>), P((TEXT,null,null),0), L(</foo>)]", + parsed.toString()); + } + + /** + * Tests that calling addParameter(), addLiteral(), addLiteral(), + * addParameter() in sequence results in the expected ParsedHtmlTemplate. + * + * <p>In particular, two calls to addLiteral() in sequence should result in + * only a single LiteralChunk. + */ + public void testAddParameterAddLiteralSequence() { + ParsedHtmlTemplate parsed = new ParsedHtmlTemplate(); + + parsed.addParameter(new ParameterChunk(new HtmlContext( + HtmlContext.Type.TEXT), 0)); + parsed.addLiteral("<a"); + parsed.addLiteral(" href=\""); + parsed.addParameter(new ParameterChunk(new HtmlContext( + HtmlContext.Type.ATTRIBUTE_START, "a", "href"), 1)); + + List<TemplateChunk> chunks = parsed.getChunks(); + assertEquals(3, chunks.size()); + Iterator<TemplateChunk> it = chunks.iterator(); + + LiteralChunk litChunk; + ParameterChunk paramChunk; + + paramChunk = (ParameterChunk) it.next(); + assertEquals(TemplateChunk.Kind.PARAMETER, paramChunk.getKind()); + assertEquals(HtmlContext.Type.TEXT, paramChunk.getContext().getType()); + assertNull(paramChunk.getContext().getTag()); + assertNull(paramChunk.getContext().getAttribute()); + assertEquals(0, paramChunk.getParameterIndex()); + assertEquals("P((TEXT,null,null),0)", paramChunk.toString()); + + litChunk = (LiteralChunk) it.next(); + assertEquals(TemplateChunk.Kind.LITERAL, litChunk.getKind()); + assertEquals("<a href=\"", litChunk.getLiteral()); + assertEquals("L(<a href=\")", litChunk.toString()); + + paramChunk = (ParameterChunk) it.next(); + assertEquals(TemplateChunk.Kind.PARAMETER, paramChunk.getKind()); + assertEquals( + HtmlContext.Type.ATTRIBUTE_START, paramChunk.getContext().getType()); + assertEquals("a", paramChunk.getContext().getTag()); + assertEquals("href", paramChunk.getContext().getAttribute()); + assertEquals(1, paramChunk.getParameterIndex()); + assertEquals("P((ATTRIBUTE_START,a,href),1)", paramChunk.toString()); + + assertEquals( + "[P((TEXT,null,null),0), L(<a href=\"), " + + "P((ATTRIBUTE_START,a,href),1)]", + parsed.toString()); + } +}