/* * 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.html.internal; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.wicket.Component; import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.IMarkupFragment; import org.apache.wicket.markup.MarkupException; import org.apache.wicket.markup.MarkupStream; import org.apache.wicket.markup.WicketTag; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.head.PageHeaderItem; import org.apache.wicket.markup.head.internal.HeaderResponse; import org.apache.wicket.markup.html.TransparentWebMarkupContainer; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.renderStrategy.AbstractHeaderRenderStrategy; import org.apache.wicket.request.Response; import org.apache.wicket.response.StringResponse; /** * The HtmlHeaderContainer is automatically created and added to the component hierarchy by a * HtmlHeaderResolver instance. HtmlHeaderContainer tries to handle/render the <head> tag and * its body. However depending on the parent component, the behavior must be different. E.g. if * parent component is a Page all components of the page's hierarchy must be asked if they have * something to contribute to the <head> section of the html response. If yes, it must * <b>immediately</b> be rendered. * <p> * <head> regions may contain additional wicket components, which can be added by means of * add(Component) as usual. * <p> * <wicket:head> tags are handled by simple {@link WebMarkupContainer}s also created by * {@link org.apache.wicket.markup.resolver.HtmlHeaderResolver}. * <p> * <ul> * <li><head> will be inserted in output automatically if required</li> * <li><head> is <b>not</b> a wicket specific tag and you must use add() to add components * referenced in body of the head tag</li> * <li><head> is supported by panels, borders and inherited markup, but is <b>not</b> copied * to the output. They are for previewability only (except on Pages)</li> * <li><wicket:head> does not make sense in page markup (but does in inherited page markup)</li> * <li><wicket:head> makes sense in Panels, Borders and inherited markup (of Panels, Borders * and Pages)</li> * <li>components within <wicket:head> must be added by means of add(), like always with * Wicket. No difference.</li> * <li><wicket:head> and it's content is copied to the output. Components contained in * <wicket:head> are rendered as usual</li> * </ul> * * @author Juergen Donnerstag */ public class HtmlHeaderContainer extends TransparentWebMarkupContainer { private static final long serialVersionUID = 1L; /** * wicket:head tags (components) must only be added once. To allow for a little bit more * control, each wicket:head has an associated scope which by default is equal to the java class * name directly associated with the markup which contains the wicket:head. It can be modified * by means of the scope attribute. */ private transient Map<String, List<String>> renderedComponentsPerScope; /** * Header response that is responsible for filtering duplicate contributions. */ private transient IHeaderResponse headerResponse = null; /** * Combines the {@link MarkupStream} with the open tag, together representing the header section * in the markup. * * @author papegaaij */ public static class HeaderStreamState { private final MarkupStream markupStream; private final ComponentTag openTag; private HeaderStreamState(MarkupStream markupStream, ComponentTag openTag) { this.markupStream = markupStream; this.openTag = openTag; } /** * @return the {@link MarkupStream} */ public MarkupStream getMarkupStream() { return markupStream; } /** * @return the {@link ComponentTag} that represents the open tag */ public ComponentTag getOpenTag() { return openTag; } } /** * Construct * * @see Component#Component(String) */ public HtmlHeaderContainer(final String id) { super(id); // We will render the tags manually, because if no component asked to // contribute to the header, the tags will not be printed either. // No contribution usually only happens if none of the components // including the page does have a <head> or <wicket:head> tag. setRenderBodyOnly(true); setAuto(true); } /** * First render the body of the component. And if it is the header component of a Page (compared * to a Panel or Border), then get the header sections from all component in the hierarchy and * render them as well. */ @Override public final void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) { // We are able to automatically add <head> to the page if it is // missing. But we only want to add it, if we have content to be // written to its body. Thus we first write the output into a // StringResponse and if not empty, we copy it to the original // web response. // Temporarily replace the web response with a String response final Response webResponse = getResponse(); try { // Create a (string) response for all headers contributed by any component on the Page. final StringResponse response = new StringResponse(); getRequestCycle().setResponse(response); IHeaderResponse headerResponse = getHeaderResponse(); if (!response.equals(headerResponse.getResponse())) { getRequestCycle().setResponse(headerResponse.getResponse()); } // Render the header sections of all components on the page AbstractHeaderRenderStrategy.get().renderHeader(this, new HeaderStreamState(markupStream, openTag), getPage()); // Close the header response before rendering the header container itself // See https://issues.apache.org/jira/browse/WICKET-3728 headerResponse.close(); // Cleanup extraneous CR and LF from the response CharSequence output = getCleanResponse(response); // Automatically add <head> if necessary if (output.length() > 0) { if (renderOpenAndCloseTags()) { webResponse.write("<head>"); } webResponse.write(output); if (renderOpenAndCloseTags()) { webResponse.write("</head>"); } } } finally { // Restore the original response getRequestCycle().setResponse(webResponse); } } /** * Renders the content of the <head> section of the page, including <wicket:head> * sections in subclasses of the page. For every child-component, the content is rendered to a * string and passed to {@link IHeaderResponse}. * * @param headerStreamState */ public void renderHeaderTagBody(HeaderStreamState headerStreamState) { if (headerStreamState == null) return; final Response oldResponse = getRequestCycle().getResponse(); try { // Create a separate (string) response for the header container itself final StringResponse bodyResponse = new StringResponse(); getRequestCycle().setResponse(bodyResponse); // render the header section directly associated with the markup super.onComponentTagBody(headerStreamState.getMarkupStream(), headerStreamState.getOpenTag()); CharSequence bodyOutput = getCleanResponse(bodyResponse); if (bodyOutput.length() > 0) { getHeaderResponse().render(new PageHeaderItem(bodyOutput)); } } finally { getRequestCycle().setResponse(oldResponse); } } /** * * @param response * @return Cleaned up response */ private CharSequence getCleanResponse(final StringResponse response) { CharSequence output = response.getBuffer(); if (output.length() > 0) { if (output.charAt(0) == '\r') { for (int i = 2; i < output.length(); i += 2) { char ch = output.charAt(i); if (ch != '\r') { output = output.subSequence(i - 2, output.length()); break; } } } else if (output.charAt(0) == '\n') { for (int i = 1; i < output.length(); i++) { char ch = output.charAt(i); if (ch != '\n') { output = output.subSequence(i - 1, output.length()); break; } } } } return output; } /** * * @return True if open and close tag are to be rendered. */ protected boolean renderOpenAndCloseTags() { return true; } /** * Check if the header component is ok to render within the scope given. * * @param scope * The scope of the header component * @param id * The component's id * @return true, if the component ok to render */ public boolean okToRenderComponent(final String scope, final String id) { if (renderedComponentsPerScope == null) { renderedComponentsPerScope = new HashMap<String, List<String>>(); } List<String> componentScope = renderedComponentsPerScope.get(scope); if (componentScope == null) { componentScope = new ArrayList<String>(); renderedComponentsPerScope.put(scope, componentScope); } if (componentScope.contains(id)) { return false; } componentScope.add(id); return true; } @Override protected void onAfterRender() { super.onAfterRender(); renderedComponentsPerScope = null; headerResponse = null; } /** * Factory method for creating header response * * @return new header response */ protected IHeaderResponse newHeaderResponse() { return new HeaderResponse() { @Override protected Response getRealResponse() { return HtmlHeaderContainer.this.getResponse(); } }; } /** * Returns the header response. * * @return header response */ public IHeaderResponse getHeaderResponse() { if (headerResponse == null) { headerResponse = getApplication().decorateHeaderResponse(newHeaderResponse()); } return headerResponse; } @Override public IMarkupFragment getMarkup() { if (getParent() == null) { throw new WicketRuntimeException( "Bug: The Wicket internal instance of HtmlHeaderContainer is not connected to a parent"); } // Get the page markup IMarkupFragment markup = getPage().getMarkup(); if (markup == null) { throw new MarkupException("Unable to get page markup: " + getPage().toString()); } // Find the markup fragment MarkupStream stream = new MarkupStream(markup); IMarkupFragment headerMarkup = null; while (stream.skipUntil(ComponentTag.class)) { ComponentTag tag = stream.getTag(); if (tag.isOpen() || tag.isOpenClose()) { if (tag instanceof WicketTag) { WicketTag wtag = (WicketTag)tag; if (wtag.isHeadTag() || wtag.isHeaderItemsTag()) { headerMarkup = stream.getMarkupFragment(); break; } } else if (tag.getName().equalsIgnoreCase("head") && tag.isAutoComponentTag()) { headerMarkup = stream.getMarkupFragment(); break; } } stream.next(); } setMarkup(headerMarkup); return headerMarkup; } }