/*
* 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.markup.parser.filter;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.Markup;
import org.apache.wicket.markup.MarkupElement;
import org.apache.wicket.markup.MarkupException;
import org.apache.wicket.markup.WicketParseException;
import org.apache.wicket.markup.parser.AbstractMarkupFilter;
import org.apache.wicket.util.collections.ArrayListStack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is a markup inline filter. It identifies HTML specific issues which make HTML not 100% xml
* compliant. E.g. tags like <p> often are missing the corresponding close tag.
*
* @author Juergen Donnerstag
*/
public final class HtmlHandler extends AbstractMarkupFilter
{
/** Logging */
private static final Logger log = LoggerFactory.getLogger(HtmlHandler.class);
/** Tag stack to find balancing tags */
final private ArrayListStack<ComponentTag> stack = new ArrayListStack<ComponentTag>();
/** Map of simple tags. */
private static final Map<String, Boolean> doesNotRequireCloseTag = new HashMap<String, Boolean>();
static
{
// Tags which are allowed not be closed in HTML
// http://www.w3.org/TR/html5/syntax.html#void-elements
// HTML5 doesn't allow void <p> but we should keep it for backward compatibility
doesNotRequireCloseTag.put("p", Boolean.TRUE);
doesNotRequireCloseTag.put("br", Boolean.TRUE);
doesNotRequireCloseTag.put("img", Boolean.TRUE);
doesNotRequireCloseTag.put("input", Boolean.TRUE);
doesNotRequireCloseTag.put("hr", Boolean.TRUE);
doesNotRequireCloseTag.put("link", Boolean.TRUE);
doesNotRequireCloseTag.put("meta", Boolean.TRUE);
doesNotRequireCloseTag.put("area", Boolean.TRUE);
doesNotRequireCloseTag.put("base", Boolean.TRUE);
doesNotRequireCloseTag.put("col", Boolean.TRUE);
doesNotRequireCloseTag.put("command", Boolean.TRUE);
doesNotRequireCloseTag.put("embed", Boolean.TRUE);
doesNotRequireCloseTag.put("keygen", Boolean.TRUE);
doesNotRequireCloseTag.put("param", Boolean.TRUE);
doesNotRequireCloseTag.put("source", Boolean.TRUE);
doesNotRequireCloseTag.put("track", Boolean.TRUE);
doesNotRequireCloseTag.put("wbr", Boolean.TRUE);
}
/**
* Construct.
*/
public HtmlHandler()
{
}
@Override
public void postProcess(final Markup markup)
{
// If there's still a non-simple tag left, it's an error
while (stack.size() > 0)
{
final ComponentTag top = stack.peek();
if (!requiresCloseTag(top.getName()))
{
stack.pop();
top.setHasNoCloseTag(true);
}
else
{
throw new MarkupException(markup, "Tag does not have a close tag", null);
}
}
}
@Override
protected MarkupElement onComponentTag(final ComponentTag tag) throws ParseException
{
// Check tag type
if (tag.isOpen())
{
// Check if open tags contains a "wicket:id" component
setContainsWicketIdFlag(tag);
// Push onto stack
stack.push(tag);
}
else if (tag.isClose())
{
// Check that there is something on the stack
if (stack.size() > 0)
{
// Pop the top tag off the stack
ComponentTag top = stack.pop();
// If the name of the current close tag does not match the
// tag on the stack then we may have a mismatched close tag
boolean mismatch = !hasEqualTagName(top, tag);
if (mismatch)
{
// Pop any simple tags off the top of the stack
while (mismatch && !requiresCloseTag(top.getName()))
{
top.setHasNoCloseTag(true);
top.setContainsWicketId(false);
// Pop simple tag
if (stack.isEmpty())
{
break;
}
top = stack.pop();
// Does new top of stack mismatch too?
mismatch = !hasEqualTagName(top, tag);
}
// If adjusting for simple tags did not fix the problem,
// it must be a real mismatch.
if (mismatch)
{
throw new ParseException("Tag " + top.toUserDebugString() +
" has a mismatched close tag at " + tag.toUserDebugString(),
top.getPos());
}
}
// Tag matches, so add pointer to matching tag
tag.setOpenTag(top);
}
else
{
throw new WicketParseException("Tag does not have a matching open tag:", tag);
}
}
else if (tag.isOpenClose())
{
// Tag closes itself
tag.setOpenTag(tag);
}
return tag;
}
/**
* Checks if the tag is a Wicket component explicitly added. i.e
* it has the "wicket:id" attribute.
*
* @param tag
*/
private void setContainsWicketIdFlag(ComponentTag tag)
{
// check if it is a wicket:id component
String wicketIdAttr = getWicketNamespace() + ":" + "id";
boolean hasWicketId = tag.getAttributes().get(wicketIdAttr) != null;
if (hasWicketId)
{
for (ComponentTag componentTag : stack)
{
componentTag.setContainsWicketId(hasWicketId);
}
}
}
/**
* Gets whether this tag does not require a closing tag.
*
* @param name
* The tag's name, e.g. a, br, div, etc.
* @return True if this tag does not require a closing tag
*/
public static boolean requiresCloseTag(final String name)
{
return doesNotRequireCloseTag.get(name.toLowerCase()) == null;
}
/**
* Compare tag name including namespace
*
* @param tag1
* @param tag2
* @return true if name and namespace are equal
*/
public static boolean hasEqualTagName(final ComponentTag tag1, final ComponentTag tag2)
{
if (!tag1.getName().equalsIgnoreCase(tag2.getName()))
{
return false;
}
if ((tag1.getNamespace() == null) && (tag2.getNamespace() == null))
{
return true;
}
if ((tag1.getNamespace() != null) && (tag2.getNamespace() != null))
{
return tag1.getNamespace().equalsIgnoreCase(tag2.getNamespace());
}
return false;
}
}