/*
* 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 org.apache.wicket.Component;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.ComponentTag.IAutoComponentFactory;
import org.apache.wicket.markup.Markup;
import org.apache.wicket.markup.MarkupElement;
import org.apache.wicket.markup.MarkupException;
import org.apache.wicket.markup.MarkupStream;
import org.apache.wicket.markup.html.internal.HtmlHeaderContainer;
import org.apache.wicket.markup.html.internal.HtmlHeaderItemsContainer;
import org.apache.wicket.markup.parser.AbstractMarkupFilter;
import org.apache.wicket.markup.parser.XmlTag.TagType;
import org.apache.wicket.markup.resolver.HtmlHeaderResolver;
import org.apache.wicket.util.tester.BaseWicketTester;
/**
* This is a markup inline filter.
* <p>
* It assumes that {@link org.apache.wicket.markup.parser.filter.WicketTagIdentifier}
* has been called first and search for a <head> tag (note: not wicket:head). Provided the markup contains a
* <body> tag it will automatically prepend a <head> tag if missing.
* </p>
* <p>
* Additionally this filter handles <wicket:header-items/>. If there is such tag then it is marked
* as the one that should be used as {@link org.apache.wicket.markup.html.internal.HtmlHeaderContainer}, by
* setting its id to {@value #HEADER_ID}.
* </p>
* <p>
* Note: This handler is only relevant for Pages (see MarkupParser.newFilterChain())
*
* @see org.apache.wicket.markup.MarkupParser
* @see org.apache.wicket.markup.resolver.HtmlHeaderResolver
* @author Juergen Donnerstag
*/
public final class HtmlHeaderSectionHandler extends AbstractMarkupFilter
{
public static final String BODY = "body";
public static final String HEAD = "head";
/** The automatically assigned wicket:id to >head< tag */
public static final String HEADER_ID = "_header_";
public static final String HEADER_ID_ITEM = "_header_item_";
/** True if <head> has been found already */
private boolean foundHead = false;
/** True if </head> has been found already */
private boolean foundClosingHead = false;
/** True if </wicket:header-items> has been found already */
private boolean foundHeaderItemsTag = false;
/** True if all the rest of the markup file can be ignored */
private boolean ignoreTheRest = false;
/** The Markup available so far for the resource */
private final Markup markup;
private static final IAutoComponentFactory HTML_HEADER_FACTORY = new IAutoComponentFactory()
{
@Override
public Component newComponent(MarkupContainer container, ComponentTag tag)
{
return new HtmlHeaderContainer(tag.getId());
}
};
private static final IAutoComponentFactory HTML_HEADER_ITEMS_FACTORY = new IAutoComponentFactory()
{
@Override
public Component newComponent(MarkupContainer container, ComponentTag tag)
{
return new HtmlHeaderItemsContainer(tag.getId());
}
};
/**
* Construct.
*
* @param markup
* The Markup object being filled while reading the markup resource
*/
public HtmlHeaderSectionHandler(final Markup markup)
{
super(markup.getMarkupResourceStream());
this.markup = markup;
}
@Override
protected final MarkupElement onComponentTag(ComponentTag tag) throws ParseException
{
// Whatever there is left in the markup, ignore it
if (ignoreTheRest == true)
{
return tag;
}
// if it is <head> or </head>
if (HEAD.equalsIgnoreCase(tag.getName()))
{
if (tag.getNamespace() == null)
{
handleHeadTag(tag);
}
else
{
// we found <wicket:head>
foundHead = true;
foundClosingHead = true;
}
}
else if (HtmlHeaderResolver.HEADER_ITEMS.equalsIgnoreCase(tag.getName()) &&
tag.getNamespace().equalsIgnoreCase(getWicketNamespace()))
{
handleHeaderItemsTag(tag);
}
else if (BODY.equalsIgnoreCase(tag.getName()) && (tag.getNamespace() == null))
{
handleBodyTag();
}
return tag;
}
/**
* Handle tag <body>
*/
private void handleBodyTag()
{
// WICKET-4511: We found <body> inside <head> tag. Markup is not valid!
if (foundHead && !foundClosingHead)
{
throw new MarkupException(new MarkupStream(markup),
"Invalid page markup. Tag <BODY> found inside <HEAD>");
}
// We found <body>
if (foundHead == false)
{
insertHeadTag();
}
// <head> must always be before <body>
ignoreTheRest = true;
}
/**
* Handle tag <wicket:header-items>
*
* @param tag
*/
private void handleHeaderItemsTag(ComponentTag tag)
{
if ((tag.isOpen() || tag.isOpenClose()) && foundHeaderItemsTag)
{
throw new MarkupException(new MarkupStream(markup),
"More than one <wicket:header-items/> detected in the <head> element. Only one is allowed.");
}
else if (foundClosingHead)
{
throw new MarkupException(new MarkupStream(markup),
"Detected <wicket:header-items/> after the closing </head> element.");
}
foundHeaderItemsTag = true;
tag.setId(HEADER_ID);
tag.setAutoComponentTag(true);
tag.setModified(true);
tag.setAutoComponentFactory(HTML_HEADER_ITEMS_FACTORY);
}
/**
* Handle tag <head>
* @param tag
*/
private void handleHeadTag(ComponentTag tag)
{
// we found <head>
if (tag.isOpen())
{
if(foundHead)
{
throw new MarkupException(new MarkupStream(markup),
"Tag <head> is not allowed at this position (do you have multiple <head> tags in your markup?).");
}
foundHead = true;
if (tag.getId() == null)
{
tag.setId(HEADER_ID);
tag.setAutoComponentTag(true);
tag.setModified(true);
tag.setAutoComponentFactory(HTML_HEADER_FACTORY);
}
}
else if (tag.isClose())
{
if (foundHeaderItemsTag)
{
// revert the settings from above
ComponentTag headOpenTag = tag.getOpenTag();
// change the id because it is special. See HtmlHeaderResolver
headOpenTag.setId(HEADER_ID + "-Ignored");
headOpenTag.setAutoComponentTag(false);
headOpenTag.setModified(false);
headOpenTag.setFlag(ComponentTag.RENDER_RAW, true);
headOpenTag.setAutoComponentFactory(null);
}
foundClosingHead = true;
}
}
/**
* Insert <head> open and close tag (with empty body) to the current position.
*/
private void insertHeadTag()
{
// Note: only the open-tag must be a AutoComponentTag
final ComponentTag openTag = new ComponentTag(HEAD, TagType.OPEN);
openTag.setId(HEADER_ID);
openTag.setAutoComponentTag(true);
openTag.setModified(true);
openTag.setAutoComponentFactory(HTML_HEADER_FACTORY);
final ComponentTag closeTag = new ComponentTag(HEAD, TagType.CLOSE);
closeTag.setOpenTag(openTag);
closeTag.setModified(true);
// insert the tags into the markup stream
markup.addMarkupElement(openTag);
markup.addMarkupElement(closeTag);
}
}