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.panel; 018 019import org.apache.wicket.Component; 020import org.apache.wicket.MarkupContainer; 021import org.apache.wicket.WicketRuntimeException; 022import org.apache.wicket.markup.ComponentTag; 023import org.apache.wicket.markup.IMarkupFragment; 024import org.apache.wicket.markup.MarkupElement; 025import org.apache.wicket.markup.MarkupNotFoundException; 026import org.apache.wicket.markup.MarkupStream; 027import org.apache.wicket.markup.TagUtils; 028import org.apache.wicket.markup.WicketTag; 029import org.apache.wicket.markup.html.HeaderPartContainer; 030import org.apache.wicket.markup.html.MarkupUtil; 031import org.apache.wicket.markup.html.WebMarkupContainer; 032import org.apache.wicket.markup.html.internal.HtmlHeaderContainer; 033import org.apache.wicket.util.lang.Args; 034import org.apache.wicket.util.lang.Classes; 035 036/** 037 * Boilerplate for a markup sourcing strategy which retrieves the markup from associated markup 038 * files. 039 * 040 * @author Juergen Donnerstag 041 */ 042public abstract class AssociatedMarkupSourcingStrategy extends AbstractMarkupSourcingStrategy 043{ 044 private final String tagName; 045 046 /** 047 * Constructor. 048 * 049 * @param tagName 050 * Either "panel" or "border" 051 */ 052 public AssociatedMarkupSourcingStrategy(final String tagName) 053 { 054 this.tagName = Args.notNull(tagName, "tagName"); 055 } 056 057 @Override 058 public void onComponentTag(final Component component, final ComponentTag tag) 059 { 060 super.onComponentTag(component, tag); 061 062 // In case you want to copy attributes from <wicket:panel> tag to the "calling" tag, you 063 // may subclass onComponentTag of your component and call TagUtils.copyAttributes(). 064 } 065 066 /** 067 * Render the associated markup markup 068 * 069 * @param component 070 */ 071 protected final void renderAssociatedMarkup(final Component component) 072 { 073 ((MarkupContainer)component).renderAssociatedMarkup(tagName); 074 } 075 076 /** 077 * Search for the child's markup in the associated markup file. 078 * 079 * @param parent 080 * The container expected to contain the markup for child 081 * @param child 082 * The child component to find the markup for 083 * @return The markup associated with the child 084 */ 085 @Override 086 public IMarkupFragment getMarkup(final MarkupContainer parent, final Component child) 087 { 088 Args.notNull(tagName, "tagName"); 089 090 IMarkupFragment associatedMarkup = parent.getAssociatedMarkup(); 091 if (associatedMarkup == null) 092 { 093 throw new MarkupNotFoundException("Failed to find markup file associated. " + 094 Classes.simpleName(parent.getClass()) + ": " + parent); 095 } 096 097 // Find <wicket:panel> 098 IMarkupFragment markup = MarkupUtil.findStartTag(associatedMarkup, tagName); 099 if (markup == null) 100 { 101 throw new MarkupNotFoundException("Expected to find <wicket:" + tagName + 102 "> in associated markup file. Markup: " + associatedMarkup); 103 } 104 105 // If child == null, than return the markup fragment starting with <wicket:panel> 106 if (child == null) 107 { 108 //clean any markup previously loaded for children 109 cleanChildrenMarkup(parent); 110 111 return markup; 112 } 113 114 // Find the markup for the child component 115 associatedMarkup = markup.find(child.getId()); 116 if (associatedMarkup != null) 117 { 118 return associatedMarkup; 119 } 120 121 associatedMarkup = searchMarkupInTransparentResolvers(parent, markup, child); 122 if (associatedMarkup != null) 123 { 124 return associatedMarkup; 125 } 126 127 return findMarkupInAssociatedFileHeader(parent, child); 128 } 129 130 /** 131 * Iterate on parent's children and set their markup to null. 132 * 133 * @param parent 134 */ 135 private void cleanChildrenMarkup(MarkupContainer parent) 136 { 137 for (Component child : parent) 138 { 139 child.setMarkup(null); 140 } 141 } 142 143 /** 144 * Search the child's markup in the header section of the markup 145 * 146 * @param container 147 * @param child 148 * @return Null, if not found 149 */ 150 public IMarkupFragment findMarkupInAssociatedFileHeader(final MarkupContainer container, 151 final Component child) 152 { 153 // Get the associated markup 154 IMarkupFragment markup = container.getAssociatedMarkup(); 155 IMarkupFragment childMarkup = null; 156 157 // MarkupStream is good at searching markup 158 MarkupStream stream = new MarkupStream(markup); 159 while (stream.skipUntil(ComponentTag.class) && (childMarkup == null)) 160 { 161 ComponentTag tag = stream.getTag(); 162 if (TagUtils.isWicketHeadTag(tag)) 163 { 164 if (tag.getMarkupClass() == null) 165 { 166 // find() can still fail and return null => continue the search 167 childMarkup = stream.getMarkupFragment().find(child.getId()); 168 } 169 } 170 else if (TagUtils.isHeadTag(tag)) 171 { 172 // find() can still fail and return null => continue the search 173 childMarkup = stream.getMarkupFragment().find(child.getId()); 174 } 175 176 // Must be a direct child. We are not interested in grand children 177 if (tag.isOpen() && !tag.hasNoCloseTag()) 178 { 179 stream.skipToMatchingCloseTag(tag); 180 } 181 stream.next(); 182 } 183 184 return childMarkup; 185 } 186 187 /** 188 * Render the header from the associated markup file 189 */ 190 @Override 191 public void renderHead(final Component component, HtmlHeaderContainer container) 192 { 193 if (!(component instanceof WebMarkupContainer)) 194 { 195 throw new WicketRuntimeException(Classes.simpleName(component.getClass()) + 196 " can only be associated with WebMarkupContainer."); 197 } 198 199 renderHeadFromAssociatedMarkupFile((WebMarkupContainer)component, container); 200 } 201 202 /** 203 * Called by components like Panel and Border which have associated Markup and which may have a 204 * <wicket:head> tag. 205 * <p> 206 * Whereas 'this' might be a Panel or Border, the HtmlHeaderContainer parameter has been added 207 * to the Page as a container for all headers any of its components might wish to contribute to. 208 * <p> 209 * The headers contributed are rendered in the standard way. 210 * 211 * @param container 212 * @param htmlContainer 213 * The HtmlHeaderContainer added to the Page 214 */ 215 public final void renderHeadFromAssociatedMarkupFile(final WebMarkupContainer container, 216 final HtmlHeaderContainer htmlContainer) 217 { 218 // Gracefully getAssociateMarkupStream. Throws no exception in case 219 // markup is not found 220 final MarkupStream markupStream = container.getAssociatedMarkupStream(false); 221 if (markupStream == null) 222 { 223 return; 224 } 225 226 // Position pointer at current (first) header 227 while (nextHeaderMarkup(markupStream) != -1) 228 { 229 // found <wicket:head> 230 String headerId = getHeaderId(container, markupStream); 231 232 // Create a HeaderPartContainer and associate the markup 233 HeaderPartContainer headerPart = getHeaderPart(container, headerId, 234 markupStream.getMarkupFragment()); 235 if (headerPart != null) 236 { 237 // A component's header section must only be added once, 238 // no matter how often the same Component has been added 239 // to the page or any other container in the hierarchy. 240 if (htmlContainer.okToRenderComponent(headerPart.getScope(), headerPart.getId())) 241 { 242 // make sure the Page is accessible 243 headerPart.setParent(htmlContainer); 244 headerPart.render(); 245 } 246 } 247 248 // Position the stream after <wicket:head> 249 markupStream.skipComponent(); 250 } 251 } 252 253 /** 254 * 255 * @param container 256 * @param markupStream 257 * @return The header id 258 */ 259 private String getHeaderId(final Component container, final MarkupStream markupStream) 260 { 261 Class<?> markupClass = markupStream.getTag().getMarkupClass(); 262 if (markupClass == null) 263 { 264 markupClass = markupStream.getContainerClass(); 265 } 266 267 // create a unique id for the HtmlHeaderContainer 268 StringBuilder builder = new StringBuilder(100); 269 builder.append('_'); 270 builder.append(Classes.simpleName(markupClass)); 271 if (container.getVariation() != null) 272 { 273 builder.append(container.getVariation()); 274 } 275 builder.append("Header"); 276 builder.append(markupStream.getCurrentIndex()); 277 return builder.toString(); 278 } 279 280 /** 281 * Gets the header part of the Panel/Border. Returns null if it doesn't have a header tag. 282 * 283 * @param container 284 * @param id 285 * @param markup 286 * @return the header part for this panel/border or null if it doesn't have a wicket:head tag. 287 */ 288 private HeaderPartContainer getHeaderPart(final WebMarkupContainer container, 289 final String id, final IMarkupFragment markup) 290 { 291 // Create a HtmlHeaderContainer for the header tag found 292 final MarkupElement element = markup.get(0); 293 if (element instanceof WicketTag) 294 { 295 final WicketTag wTag = (WicketTag)element; 296 if ((wTag.isHeadTag()) && (wTag.getNamespace() != null)) 297 { 298 // Create the header container and associate the markup with it 299 return new HeaderPartContainer(id, container, markup); 300 } 301 } 302 303 throw new WicketRuntimeException("Programming error: expected a WicketTag: " + markup); 304 } 305 306 /** 307 * Process next header markup fragment. 308 * 309 * @param associatedMarkupStream 310 * @return index or -1 when done 311 */ 312 private int nextHeaderMarkup(final MarkupStream associatedMarkupStream) 313 { 314 // No associated markup => no header section 315 if (associatedMarkupStream == null) 316 { 317 return -1; 318 } 319 320 // Scan the markup for <wicket:head>. 321 MarkupElement elem = associatedMarkupStream.get(); 322 while (elem != null) 323 { 324 if (elem instanceof WicketTag) 325 { 326 WicketTag tag = (WicketTag)elem; 327 if (tag.isOpen() && tag.isHeadTag()) 328 { 329 return associatedMarkupStream.getCurrentIndex(); 330 } 331 } 332 elem = associatedMarkupStream.next(); 333 } 334 335 // No (more) wicket:head found 336 return -1; 337 } 338}