/* * Copyright (c) 2013, the Dart project authors. * * Licensed under the Eclipse Public License v1.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.eclipse.org/legal/epl-v10.html * * 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.dart.engine.html.parser; import com.google.dart.engine.html.ast.HtmlUnit; import com.google.dart.engine.html.ast.XmlAttributeNode; import com.google.dart.engine.html.ast.XmlNode; import com.google.dart.engine.html.ast.XmlTagNode; import com.google.dart.engine.html.ast.visitor.RecursiveXmlVisitor; import com.google.dart.engine.html.scanner.Token; import com.google.dart.engine.html.scanner.TokenType; import com.google.dart.engine.utilities.io.PrintStringWriter; import static com.google.dart.engine.html.scanner.TokenType.EOF; import junit.framework.Assert; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import java.util.ArrayList; /** * Instances of {@code XmlValidator} traverse an {@link XmlNode} structure and validate the node * hierarchy. */ public class XmlValidator extends RecursiveXmlVisitor<Void> { public static class Attributes { final String[] keyValuePairs; public Attributes(String... keyValuePairs) { this.keyValuePairs = keyValuePairs; } } public static class Tag { final String tag; final Attributes attributes; final String content; final Tag[] children; public Tag(String tag, Attributes attributes, String content, Tag... children) { this.tag = tag; this.attributes = attributes; this.content = content; this.children = children; } } /** * A list containing the errors found while traversing the AST structure. */ private ArrayList<String> errors = new ArrayList<String>(); /** * The tags to expect when visiting or {@code null} if tags should not be checked. */ private Tag[] expectedTagsInOrderVisited; /** * The current index into the {@link #expectedTagsInOrderVisited} array. */ private int expectedTagsIndex; /** * The key/value pairs to expect when visiting or {@code null} if attributes should not be * checked. */ private String[] expectedAttributeKeyValuePairs; /** * The current index into the {@link #expectedAttributeKeyValuePairs}. */ private int expectedAttributeIndex; /** * Assert that no errors were found while traversing any of the AST structures that have been * visited. */ public void assertValid() { while (expectedTagsIndex < expectedTagsInOrderVisited.length) { String expectedTag = expectedTagsInOrderVisited[expectedTagsIndex++].tag; errors.add("Expected to visit node with tag: " + expectedTag); } if (!errors.isEmpty()) { @SuppressWarnings("resource") PrintStringWriter writer = new PrintStringWriter(); writer.print("Invalid XML structure:"); for (String message : errors) { writer.println(); writer.print(" "); writer.print(message); } Assert.fail(writer.toString()); } } /** * Set the tags to be expected when visiting * * @param expectedTags the expected tags */ public void expectTags(Tag... expectedTags) { // Flatten the hierarchy into expected order in which the tags are visited ArrayList<Tag> expected = new ArrayList<Tag>(); expectTags(expected, expectedTags); this.expectedTagsInOrderVisited = expected.toArray(new Tag[expected.size()]); } @Override public Void visitHtmlUnit(HtmlUnit node) { if (node.getParent() != null) { errors.add("HtmlUnit should not have a parent"); } if (node.getEndToken().getType() != EOF) { errors.add("HtmlUnit end token should be of type EOF"); } validateNode(node); return super.visitHtmlUnit(node); } @Override public Void visitXmlAttributeNode(XmlAttributeNode actual) { if (!(actual.getParent() instanceof XmlTagNode)) { errors.add("Expected " + actual.getClass().getSimpleName() + " to have parent of type " + XmlTagNode.class.getSimpleName()); } String actualName = actual.getName(); String actualValue = actual.getValueToken().getLexeme(); if (expectedAttributeIndex < expectedAttributeKeyValuePairs.length) { String expectedName = expectedAttributeKeyValuePairs[expectedAttributeIndex]; if (!expectedName.equals(actualName)) { errors.add("Expected " + (expectedTagsIndex - 1) + " tag: " + expectedTagsInOrderVisited[expectedTagsIndex - 1].tag + " attribute " + (expectedAttributeIndex / 2) + " to have name: " + expectedName + " but found: " + actualName); } String expectedValue = expectedAttributeKeyValuePairs[expectedAttributeIndex + 1]; if (!expectedValue.equals(actualValue)) { errors.add("Expected " + (expectedTagsIndex - 1) + " tag: " + expectedTagsInOrderVisited[expectedTagsIndex - 1].tag + " attribute " + (expectedAttributeIndex / 2) + " to have value: " + expectedValue + " but found: " + actualValue); } } else { errors.add("Unexpected " + (expectedTagsIndex - 1) + " tag: " + expectedTagsInOrderVisited[expectedTagsIndex - 1].tag + " attribute " + (expectedAttributeIndex / 2) + " name: " + actualName + " value: " + actualValue); } expectedAttributeIndex += 2; validateNode(actual); return super.visitXmlAttributeNode(actual); } @Override public Void visitXmlTagNode(XmlTagNode actual) { if (!(actual.getParent() instanceof HtmlUnit || actual.getParent() instanceof XmlTagNode)) { errors.add("Expected " + actual.getClass().getSimpleName() + " to have parent of type " + HtmlUnit.class.getSimpleName() + " or " + XmlTagNode.class.getSimpleName()); } if (expectedTagsInOrderVisited != null) { String actualTag = actual.getTag(); if (expectedTagsIndex < expectedTagsInOrderVisited.length) { Tag expected = expectedTagsInOrderVisited[expectedTagsIndex]; if (!expected.tag.equals(actualTag)) { errors.add("Expected " + expectedTagsIndex + " tag: " + expected.tag + " but found: " + actualTag); } expectedAttributeKeyValuePairs = expected.attributes.keyValuePairs; int expectedAttributeCount = expectedAttributeKeyValuePairs.length / 2; int actualAttributeCount = actual.getAttributes().size(); if (expectedAttributeCount != actualAttributeCount) { errors.add("Expected " + expectedTagsIndex + " tag: " + expected.tag + " to have " + expectedAttributeCount + " attributes but found " + actualAttributeCount); } expectedAttributeIndex = 0; expectedTagsIndex++; assertNotNull(actual.getAttributeEnd()); assertNotNull(actual.getContentEnd()); int count = 0; Token token = actual.getAttributeEnd().getNext(); Token lastToken = actual.getContentEnd(); while (token != lastToken) { token = token.getNext(); if (++count > 1000) { Assert.fail("Expected " + expectedTagsIndex + " tag: " + expected.tag + " to have a sequence of tokens from getAttributeEnd() to getContentEnd()"); break; } } if (actual.getAttributeEnd().getType() == TokenType.GT) { if (HtmlParser.SELF_CLOSING.contains(actual.getTag())) { assertNull(actual.getClosingTag()); } else { assertNotNull(actual.getClosingTag()); } } else if (actual.getAttributeEnd().getType() == TokenType.SLASH_GT) { assertNull(actual.getClosingTag()); } else { Assert.fail("Unexpected attribute end token: " + actual.getAttributeEnd().getLexeme()); } if (expected.content != null && !expected.content.equals(actual.getContent())) { errors.add("Expected " + expectedTagsIndex + " tag: " + expected.tag + " to have content '" + expected.content + "' but found '" + actual.getContent() + "'"); } if (expected.children.length != actual.getTagNodes().size()) { errors.add("Expected " + expectedTagsIndex + " tag: " + expected.tag + " to have " + expected.children.length + " children but found " + actual.getTagNodes().size()); } else { for (int index = 0; index < expected.children.length; index++) { String expectedChildTag = expected.children[index].tag; String actualChildTag = actual.getTagNodes().get(index).getTag(); if (!expectedChildTag.equals(actualChildTag)) { errors.add("Expected " + expectedTagsIndex + " tag: " + expected.tag + " child " + index + " to have tag: " + expectedChildTag + " but found: " + actualChildTag); } } } } else { errors.add("Visited unexpected tag: " + actualTag); } } validateNode(actual); return super.visitXmlTagNode(actual); } /** * Append the specified tags to the array in depth first order * * @param expected the array to which the tags are added (not {@code null}) * @param expectedTags the expected tags to be added (not {@code null}, contains no {@code null}s) */ private void expectTags(ArrayList<Tag> expected, Tag... expectedTags) { for (Tag tag : expectedTags) { expected.add(tag); expectTags(expected, tag.children); } } private void validateNode(XmlNode node) { if (node.getBeginToken() == null) { errors.add("No begin token for " + node.getClass().getName()); } if (node.getEndToken() == null) { errors.add("No end token for " + node.getClass().getName()); } int nodeStart = node.getOffset(); int nodeLength = node.getLength(); if (nodeStart < 0 || nodeLength < 0) { errors.add("No source info for " + node.getClass().getName()); } XmlNode parent = node.getParent(); if (parent != null) { int nodeEnd = nodeStart + nodeLength; int parentStart = parent.getOffset(); int parentEnd = parentStart + parent.getLength(); if (nodeStart < parentStart) { errors.add("Invalid source start (" + nodeStart + ") for " + node.getClass().getName() + " inside " + parent.getClass().getName() + " (" + parentStart + ")"); } if (nodeEnd > parentEnd) { errors.add("Invalid source end (" + nodeEnd + ") for " + node.getClass().getName() + " inside " + parent.getClass().getName() + " (" + parentStart + ")"); } } } }