001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.markup.html.internal; 018 019import java.util.ArrayList; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023 024import org.apache.wicket.Component; 025import org.apache.wicket.WicketRuntimeException; 026import org.apache.wicket.markup.ComponentTag; 027import org.apache.wicket.markup.IMarkupFragment; 028import org.apache.wicket.markup.MarkupException; 029import org.apache.wicket.markup.MarkupStream; 030import org.apache.wicket.markup.WicketTag; 031import org.apache.wicket.markup.head.IHeaderResponse; 032import org.apache.wicket.markup.head.PageHeaderItem; 033import org.apache.wicket.markup.head.internal.HeaderResponse; 034import org.apache.wicket.markup.html.TransparentWebMarkupContainer; 035import org.apache.wicket.markup.html.WebMarkupContainer; 036import org.apache.wicket.markup.renderStrategy.AbstractHeaderRenderStrategy; 037import org.apache.wicket.request.Response; 038import org.apache.wicket.response.StringResponse; 039 040 041/** 042 * The HtmlHeaderContainer is automatically created and added to the component hierarchy by a 043 * HtmlHeaderResolver instance. HtmlHeaderContainer tries to handle/render the <head> tag and 044 * its body. However depending on the parent component, the behavior must be different. E.g. if 045 * parent component is a Page all components of the page's hierarchy must be asked if they have 046 * something to contribute to the <head> section of the html response. If yes, it must 047 * <b>immediately</b> be rendered. 048 * <p> 049 * <head> regions may contain additional wicket components, which can be added by means of 050 * add(Component) as usual. 051 * <p> 052 * <wicket:head> tags are handled by simple {@link WebMarkupContainer}s also created by 053 * {@link org.apache.wicket.markup.resolver.HtmlHeaderResolver}. 054 * <p> 055 * <ul> 056 * <li><head> will be inserted in output automatically if required</li> 057 * <li><head> is <b>not</b> a wicket specific tag and you must use add() to add components 058 * referenced in body of the head tag</li> 059 * <li><head> is supported by panels, borders and inherited markup, but is <b>not</b> copied 060 * to the output. They are for previewability only (except on Pages)</li> 061 * <li><wicket:head> does not make sense in page markup (but does in inherited page markup)</li> 062 * <li><wicket:head> makes sense in Panels, Borders and inherited markup (of Panels, Borders 063 * and Pages)</li> 064 * <li>components within <wicket:head> must be added by means of add(), like always with 065 * Wicket. No difference.</li> 066 * <li><wicket:head> and it's content is copied to the output. Components contained in 067 * <wicket:head> are rendered as usual</li> 068 * </ul> 069 * 070 * @author Juergen Donnerstag 071 */ 072public class HtmlHeaderContainer extends TransparentWebMarkupContainer 073{ 074 private static final long serialVersionUID = 1L; 075 076 /** 077 * wicket:head tags (components) must only be added once. To allow for a little bit more 078 * control, each wicket:head has an associated scope which by default is equal to the java class 079 * name directly associated with the markup which contains the wicket:head. It can be modified 080 * by means of the scope attribute. 081 */ 082 private transient Map<String, List<String>> renderedComponentsPerScope; 083 084 /** 085 * Header response that is responsible for filtering duplicate contributions. 086 */ 087 private transient IHeaderResponse headerResponse = null; 088 089 /** 090 * Combines the {@link MarkupStream} with the open tag, together representing the header section 091 * in the markup. 092 * 093 * @author papegaaij 094 */ 095 public static class HeaderStreamState 096 { 097 private final MarkupStream markupStream; 098 private final ComponentTag openTag; 099 100 private HeaderStreamState(MarkupStream markupStream, ComponentTag openTag) 101 { 102 this.markupStream = markupStream; 103 this.openTag = openTag; 104 } 105 106 /** 107 * @return the {@link MarkupStream} 108 */ 109 public MarkupStream getMarkupStream() 110 { 111 return markupStream; 112 } 113 114 /** 115 * @return the {@link ComponentTag} that represents the open tag 116 */ 117 public ComponentTag getOpenTag() 118 { 119 return openTag; 120 } 121 } 122 123 /** 124 * Construct 125 * 126 * @see Component#Component(String) 127 */ 128 public HtmlHeaderContainer(final String id) 129 { 130 super(id); 131 132 // We will render the tags manually, because if no component asked to 133 // contribute to the header, the tags will not be printed either. 134 // No contribution usually only happens if none of the components 135 // including the page does have a <head> or <wicket:head> tag. 136 setRenderBodyOnly(true); 137 138 setAuto(true); 139 } 140 141 /** 142 * First render the body of the component. And if it is the header component of a Page (compared 143 * to a Panel or Border), then get the header sections from all component in the hierarchy and 144 * render them as well. 145 */ 146 @Override 147 public final void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) 148 { 149 // We are able to automatically add <head> to the page if it is 150 // missing. But we only want to add it, if we have content to be 151 // written to its body. Thus we first write the output into a 152 // StringResponse and if not empty, we copy it to the original 153 // web response. 154 155 // Temporarily replace the web response with a String response 156 final Response webResponse = getResponse(); 157 158 try 159 { 160 // Create a (string) response for all headers contributed by any component on the Page. 161 final StringResponse response = new StringResponse(); 162 getRequestCycle().setResponse(response); 163 164 try (IHeaderResponse headerResponse = getHeaderResponse()) { 165 if (!response.equals(headerResponse.getResponse())) 166 { 167 getRequestCycle().setResponse(headerResponse.getResponse()); 168 } 169 170 // Render the header sections of all components on the page 171 AbstractHeaderRenderStrategy.get().renderHeader(this, 172 new HeaderStreamState(markupStream, openTag), getPage()); 173 174 // Header response will be auto-closed before rendering the header container itself 175 // See https://issues.apache.org/jira/browse/WICKET-3728 176 }; 177 178 // Cleanup extraneous CR and LF from the response 179 CharSequence output = getCleanResponse(response); 180 181 // Automatically add <head> if necessary 182 if (output.length() > 0) 183 { 184 if (renderOpenAndCloseTags()) 185 { 186 webResponse.write("<head>"); 187 } 188 189 webResponse.write(output); 190 191 if (renderOpenAndCloseTags()) 192 { 193 webResponse.write("</head>"); 194 } 195 } 196 } 197 finally 198 { 199 // Restore the original response 200 getRequestCycle().setResponse(webResponse); 201 } 202 } 203 204 /** 205 * Renders the content of the <head> section of the page, including <wicket:head> 206 * sections in subclasses of the page. For every child-component, the content is rendered to a 207 * string and passed to {@link IHeaderResponse}. 208 * 209 * @param headerStreamState 210 */ 211 public void renderHeaderTagBody(HeaderStreamState headerStreamState) 212 { 213 if (headerStreamState == null) 214 return; 215 216 final Response oldResponse = getRequestCycle().getResponse(); 217 try 218 { 219 // Create a separate (string) response for the header container itself 220 final StringResponse bodyResponse = new StringResponse(); 221 getRequestCycle().setResponse(bodyResponse); 222 223 // render the header section directly associated with the markup 224 super.onComponentTagBody(headerStreamState.getMarkupStream(), 225 headerStreamState.getOpenTag()); 226 CharSequence bodyOutput = getCleanResponse(bodyResponse); 227 if (bodyOutput.length() > 0) 228 { 229 getHeaderResponse().render(new PageHeaderItem(bodyOutput)); 230 } 231 } 232 finally 233 { 234 getRequestCycle().setResponse(oldResponse); 235 } 236 } 237 238 /** 239 * 240 * @param response 241 * @return Cleaned up response 242 */ 243 private static CharSequence getCleanResponse(final StringResponse response) 244 { 245 CharSequence output = response.getBuffer(); 246 if (output.length() > 0) 247 { 248 if (output.charAt(0) == '\r') 249 { 250 for (int i = 2; i < output.length(); i += 2) 251 { 252 char ch = output.charAt(i); 253 if (ch != '\r') 254 { 255 output = output.subSequence(i - 2, output.length()); 256 break; 257 } 258 } 259 } 260 else if (output.charAt(0) == '\n') 261 { 262 for (int i = 1; i < output.length(); i++) 263 { 264 char ch = output.charAt(i); 265 if (ch != '\n') 266 { 267 output = output.subSequence(i - 1, output.length()); 268 break; 269 } 270 } 271 } 272 } 273 return output; 274 } 275 276 /** 277 * 278 * @return True if open and close tag are to be rendered. 279 */ 280 protected boolean renderOpenAndCloseTags() 281 { 282 return true; 283 } 284 285 /** 286 * Check if the header component is ok to render within the scope given. 287 * 288 * @param scope 289 * The scope of the header component 290 * @param id 291 * The component's id 292 * @return true, if the component ok to render 293 */ 294 public boolean okToRenderComponent(final String scope, final String id) 295 { 296 if (renderedComponentsPerScope == null) 297 { 298 renderedComponentsPerScope = new HashMap<>(); 299 } 300 301 List<String> componentScope = renderedComponentsPerScope.get(scope); 302 if (componentScope == null) 303 { 304 componentScope = new ArrayList<>(); 305 renderedComponentsPerScope.put(scope, componentScope); 306 } 307 308 if (componentScope.contains(id)) 309 { 310 return false; 311 } 312 componentScope.add(id); 313 return true; 314 } 315 316 @Override 317 protected void onAfterRender() { 318 super.onAfterRender(); 319 320 renderedComponentsPerScope = null; 321 headerResponse = null; 322 } 323 324 /** 325 * Factory method for creating header response 326 * 327 * @return new header response 328 */ 329 protected IHeaderResponse newHeaderResponse() 330 { 331 return new HeaderResponse() 332 { 333 @Override 334 protected Response getRealResponse() 335 { 336 return HtmlHeaderContainer.this.getResponse(); 337 } 338 }; 339 } 340 341 /** 342 * Returns the header response. 343 * 344 * @return header response 345 */ 346 public IHeaderResponse getHeaderResponse() 347 { 348 if (headerResponse == null) 349 { 350 headerResponse = getApplication().decorateHeaderResponse(newHeaderResponse()); 351 } 352 return headerResponse; 353 } 354 355 @Override 356 public IMarkupFragment getMarkup() 357 { 358 if (getParent() == null) 359 { 360 throw new WicketRuntimeException( 361 "Bug: The Wicket internal instance of HtmlHeaderContainer is not connected to a parent"); 362 } 363 364 // Get the page markup 365 IMarkupFragment markup = getPage().getMarkup(); 366 if (markup == null) 367 { 368 throw new MarkupException("Unable to get page markup: " + getPage().toString()); 369 } 370 371 // Find the markup fragment 372 MarkupStream stream = new MarkupStream(markup); 373 IMarkupFragment headerMarkup = null; 374 while (stream.skipUntil(ComponentTag.class)) 375 { 376 ComponentTag tag = stream.getTag(); 377 if (tag.isOpen() || tag.isOpenClose()) 378 { 379 if (tag instanceof WicketTag) 380 { 381 WicketTag wtag = (WicketTag)tag; 382 if (wtag.isHeadTag() || wtag.isHeaderItemsTag()) 383 { 384 headerMarkup = stream.getMarkupFragment(); 385 break; 386 } 387 } 388 else if (tag.getName().equalsIgnoreCase("head") && tag.isAutoComponentTag()) 389 { 390 headerMarkup = stream.getMarkupFragment(); 391 break; 392 } 393 } 394 395 stream.next(); 396 } 397 398 setMarkup(headerMarkup); 399 return headerMarkup; 400 } 401}