/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.wicket.protocol.http.documentvalidation;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.util.collections.ArrayListStack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Simple class that provides a convenient programmatic way to define what an expected HTML document
* should look like and then to validate a supplied document against this template. Note that this
* validator expects very clean HTML (which should not be a problem during testing). In particular
* it expects tags to be matched and that the following tags with optional close tags are actually
* closed: p, td, th, li and option.
*
* @author Chris Turner
* @deprecated Will be removed in Wicket 9.0. Use {@link org.apache.wicket.util.tester.TagTester} or
* {@link org.apache.wicket.util.tester.WicketTestCase#executeTest(Class, PageParameters, String)} instead
*/
@Deprecated
public class HtmlDocumentValidator
{
private static final Logger log = LoggerFactory.getLogger(HtmlDocumentValidator.class);
private final List<DocumentElement> elements = new ArrayList<DocumentElement>();
private boolean skipComments = true;
private Tag workingTag;
/**
* Create the validator.
*/
public HtmlDocumentValidator()
{
}
/**
* Add a root element to the validator. This will generally be the HTML element to which all
* children are added. However, it may also be other elements to represent comments or similar.
*
* @param e
* The element to add
*/
public void addRootElement(final DocumentElement e)
{
elements.add(e);
}
/**
* Check whether the supplied document is valid against the spec that has been built up within
* the validator.
*
* @param document
* The document to validate
* @return Whether the document is valid or not
*/
public boolean isDocumentValid(final String document)
{
HtmlDocumentParser parser = new HtmlDocumentParser(document);
Iterator<DocumentElement> expectedElements = elements.iterator();
ArrayListStack<Iterator<DocumentElement>> iteratorStack = new ArrayListStack<Iterator<DocumentElement>>();
ArrayListStack<String> tagNameStack = new ArrayListStack<String>();
boolean end = false;
boolean valid = true;
while (!end)
{
int token = parser.getNextToken();
switch (token)
{
case HtmlDocumentParser.UNKNOWN :
// Error is already recorded by the parser
return false;
case HtmlDocumentParser.END :
end = true;
break;
case HtmlDocumentParser.COMMENT :
valid = validateComment(expectedElements, parser);
if (!valid)
{
end = true;
}
break;
case HtmlDocumentParser.OPEN_TAG :
valid = validateTag(expectedElements, parser);
if (!valid)
{
end = true;
}
else
{
expectedElements = saveOpenTagState(iteratorStack, expectedElements,
tagNameStack);
}
break;
case HtmlDocumentParser.OPENCLOSE_TAG :
valid = validateTag(expectedElements, parser);
if (valid)
{
valid = checkOpenCloseTag();
}
if (!valid)
{
end = true;
}
break;
case HtmlDocumentParser.CLOSE_TAG :
expectedElements = validateCloseTag(tagNameStack, parser, expectedElements,
iteratorStack);
if (expectedElements == null)
{
valid = false;
end = true;
}
break;
case HtmlDocumentParser.TEXT :
valid = validateText(expectedElements, parser);
if (!valid)
{
end = true;
}
break;
}
}
// Return the valid result
return valid;
}
/**
* Set whether to skip comments of not when validating. The default is true. If this is set to
* false then Comment elements must be added to represent each comment to be validated.
*
* @param skipComments
* Whether to skip comments or not
*/
public void setSkipComments(final boolean skipComments)
{
this.skipComments = skipComments;
}
/**
* Check whether the open close tag was actually expected to have children.
*
* @return Whether valid or not
*/
private boolean checkOpenCloseTag()
{
boolean valid = true;
if (!workingTag.getExpectedChildren().isEmpty())
{
log.error("Found tag <" + workingTag.getTag() + "/> was expected to have " +
workingTag.getExpectedChildren().size() + " child elements");
valid = false;
}
return valid;
}
/**
* Check if the supplied tag is one that expects to be closed or not.
*
* @param tag
* The tag
* @return Whether the tag requires closing or not
*/
private boolean isNonClosedTag(String tag)
{
tag = workingTag.getTag().toLowerCase();
if (tag.equals("area"))
{
return true;
}
if (tag.equals("base"))
{
return true;
}
if (tag.equals("basefont"))
{
return true;
}
if (tag.equals("bgsound"))
{
return true;
}
if (tag.equals("br"))
{
return true;
}
if (tag.equals("col"))
{
return true;
}
if (tag.equals("frame"))
{
return true;
}
if (tag.equals("hr"))
{
return true;
}
if (tag.equals("img"))
{
return true;
}
if (tag.equals("input"))
{
return true;
}
if (tag.equals("isindex"))
{
return true;
}
if (tag.equals("keygen"))
{
return true;
}
if (tag.equals("link"))
{
return true;
}
if (tag.equals("meta"))
{
return true;
}
if (tag.equals("param"))
{
return true;
}
if (tag.equals("spacer"))
{
return true;
}
if (tag.equals("wbr"))
{
return true;
}
return false;
}
/**
* Save the new open tag state and find the iterator to continue to use for processing.
*
* @param iteratorStack
* The current stack of iterators
* @param expectedElements
* The current iterator of elements
* @param tagNameStack
* The stack of open tags
* @return The iterator to continue to use
*/
private Iterator<DocumentElement> saveOpenTagState(
ArrayListStack<Iterator<DocumentElement>> iteratorStack,
Iterator<DocumentElement> expectedElements, ArrayListStack<String> tagNameStack)
{
if (!isNonClosedTag(workingTag.getTag()))
{
iteratorStack.push(expectedElements);
expectedElements = workingTag.getExpectedChildren().iterator();
tagNameStack.push(workingTag.getTag());
}
return expectedElements;
}
/**
* Validate the close tag that was found.
*
* @param tagNameStack
* The stack of tag names
* @param parser
* The parser
* @param expectedElements
* The current iterator of expected elements
* @param iteratorStack
* The stack of previous iterators
* @return The next iterator to use, or null
*/
private Iterator<DocumentElement> validateCloseTag(ArrayListStack<String> tagNameStack,
HtmlDocumentParser parser, Iterator<DocumentElement> expectedElements,
ArrayListStack<Iterator<DocumentElement>> iteratorStack)
{
if (tagNameStack.isEmpty())
{
log.error("Found closing tag </" + parser.getTag() + "> when there are no " +
"tags currently open");
expectedElements = null;
}
else
{
String expectedTag = tagNameStack.pop();
if (!expectedTag.equals(parser.getTag()))
{
log.error("Found closing tag </" + parser.getTag() + "> when we expecting " +
"the closing tag </" + expectedTag + "> instead");
expectedElements = null;
}
else
{
if (expectedElements.hasNext())
{
DocumentElement e = expectedElements.next();
log.error("Found closing tag </" + parser.getTag() + "> but we were " +
"expecting to find another child element: " + e.toString());
expectedElements = null;
}
else
{
if (iteratorStack.isEmpty())
{
log.error("Unexpected parsing error");
expectedElements = null;
}
else
{
expectedElements = iteratorStack.pop();
}
}
}
}
return expectedElements;
}
/**
* Validate the comment token that was found.
*
* @param expectedElements
* The iterator of expected elements
* @param parser
* The parser
* @return Whether the comment is valid or not
*/
private boolean validateComment(Iterator<DocumentElement> expectedElements,
HtmlDocumentParser parser)
{
boolean valid = true;
if (!skipComments)
{
if (expectedElements.hasNext())
{
DocumentElement e = expectedElements.next();
if (e instanceof Comment)
{
if (!((Comment)e).getText().equals(parser.getComment()))
{
log.error("Found comment '" + parser.getComment() + "' does not match " +
"expected comment '" + ((Comment)e).getText() + "'");
valid = false;
}
}
else
{
log.error("Found comment '" + parser.getComment() + "' was not expected. " +
"We were expecting: " + e.toString());
valid = false;
}
}
else
{
log.error("Found comment '" + parser.getComment() + "' was not expected. " +
"We were not expecting any more elements within the current tag");
valid = false;
}
}
return valid;
}
/**
* Validate the tag token that was found.
*
* @param expectedElements
* The iterator of expected elements
* @param parser
* The parser
* @return Whether the tag is valid or not
*/
private boolean validateTag(Iterator<DocumentElement> expectedElements,
HtmlDocumentParser parser)
{
boolean valid = true;
if (expectedElements.hasNext())
{
DocumentElement e = expectedElements.next();
if (e instanceof Tag)
{
workingTag = (Tag)e;
if (!workingTag.getTag().equals(parser.getTag()))
{
log.error("Found tag <" + parser.getTag() + "> does not match " +
"expected tag <" + workingTag.getTag() + ">");
valid = false;
}
else
{
Map<String, String> actualAttributes = parser.getAttributes();
Map<String, String> expectedAttributes = workingTag.getExpectedAttributes();
for (Map.Entry<String, String> entry : expectedAttributes.entrySet())
{
String name = entry.getKey();
String pattern = entry.getValue();
if (!actualAttributes.containsKey(name))
{
log.error("Tag <" + workingTag.getTag() + "> was expected to have a '" +
name + "' attribute " + "but this was not present");
valid = false;
}
String value = actualAttributes.get(name);
if (value == null)
{
log.error("Attribute " + name + " was expected but not found");
valid = false;
}
else
{
if (!value.matches(pattern))
{
log.error("The value '" + value + "' of attribute '" + name +
"' of tag <" + workingTag.getTag() +
"> was expected to match the pattern '" + pattern +
"' but it does not");
valid = false;
}
}
}
for (String name : workingTag.getIllegalAttributes())
{
if (actualAttributes.containsKey(name))
{
log.error("Tag <" + workingTag.getTag() +
"> should not have an attributed named '" + name + "'");
valid = false;
}
}
}
}
else
{
log.error("Found tag <" + parser.getTag() + "> was not expected. " +
"We were expecting: " + e.toString());
valid = false;
}
}
else
{
log.error("Found tag <" + parser.getTag() + "> was not expected. " +
"We were not expecting any more elements within the current tag");
valid = false;
}
return valid;
}
/**
* Validate the text token that was found.
*
* @param expectedElements
* The iterator of expected elements
* @param parser
* The parser
* @return Whether the text is valid or not
*/
private boolean validateText(Iterator<DocumentElement> expectedElements,
HtmlDocumentParser parser)
{
boolean valid = true;
if (expectedElements.hasNext())
{
DocumentElement e = expectedElements.next();
if (e instanceof TextContent)
{
if (!parser.getText().matches(((TextContent)e).getValue()))
{
log.error("Found text '" + parser.getText() + "' does not match " +
"expected text '" + ((TextContent)e).getValue() + "'");
valid = false;
}
}
else
{
log.error("Found text '" + parser.getText() + "' was not expected. " +
"We were expecting: " + e.toString());
valid = false;
}
}
else
{
log.error("Found text '" + parser.getText() + "' was not expected. " +
"We were not expecting any more elements within the current tag");
valid = false;
}
return valid;
}
}