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; 018 019import org.apache.wicket.Component; 020import org.apache.wicket.Page; 021import org.apache.wicket.WicketRuntimeException; 022import org.apache.wicket.markup.parser.XmlTag; 023import org.apache.wicket.markup.parser.XmlTag.TagType; 024import org.apache.wicket.markup.parser.filter.HtmlHeaderSectionHandler; 025import org.apache.wicket.util.resource.IResourceStream; 026import org.apache.wicket.util.string.Strings; 027import org.slf4j.Logger; 028import org.slf4j.LoggerFactory; 029 030 031/** 032 * A Markup class which represents merged markup, as it is required for markup inheritance. 033 * <p> 034 * The Markups are merged at load time. Deep markup hierarchies are supported. Multiple inheritance 035 * is not. 036 * <p> 037 * The markup resource file, which is associated with the markup, will be the resource of the 038 * requested markup file. The base markup resources are not. 039 * <p> 040 * Base Markup must have a <wicket:child/> tag at the position where the derived markup should 041 * be inserted. From the derived markup all tags in between <wicket:extend> and 042 * </wicket:extend> will be inserted. 043 * <p> 044 * In addition, all <wicket:head> regions are copied as well. This allows to develop completely 045 * self-contained plug & play components including javascript etc. 046 * 047 * @author Juergen Donnerstag 048 */ 049public class MergedMarkup extends Markup 050{ 051 private final static Logger log = LoggerFactory.getLogger(MergedMarkup.class); 052 053 /** 054 * Merge inherited and base markup. 055 * 056 * @param markup 057 * The inherited markup 058 * @param baseMarkup 059 * The base markup 060 * @param extendIndex 061 * Index where <wicket:extend> has been found 062 */ 063 public MergedMarkup(final Markup markup, final Markup baseMarkup, int extendIndex) 064 { 065 super(markup.getMarkupResourceStream()); 066 067 getMarkupResourceStream().setBaseMarkup(baseMarkup); 068 069 // Copy settings from derived markup 070 MarkupResourceStream baseResourceStream = baseMarkup.getMarkupResourceStream(); 071 getMarkupResourceStream().setEncoding(baseResourceStream.getEncoding()); 072 getMarkupResourceStream().setWicketNamespace(baseResourceStream.getWicketNamespace()); 073 074 if (log.isDebugEnabled()) 075 { 076 String derivedResource = Strings.afterLast(markup.getMarkupResourceStream() 077 .getResource() 078 .toString(), '/'); 079 String baseResource = Strings.afterLast(baseMarkup.getMarkupResourceStream() 080 .getResource() 081 .toString(), '/'); 082 log.debug("Merge markup: derived markup: " + derivedResource + "; base markup: " + 083 baseResource); 084 } 085 086 // Merge derived and base markup 087 merge(markup, baseMarkup, extendIndex); 088 089 if (log.isDebugEnabled()) 090 { 091 log.debug("Merge markup: " + toString()); 092 } 093 } 094 095 @Override 096 public String locationAsString() 097 { 098 /* 099 * Uses both resource locations so that if the child does not have a style and the parent 100 * does, the location is unique to this combination (or vice versa) SEE WICKET-1507 (Jeremy 101 * Thomerson) 102 */ 103 String l1 = getMarkupResourceStream().getBaseMarkup().locationAsString(); 104 String l2 = getMarkupResourceStream().locationAsString(); 105 106 if ((l1 == null) && (l2 == null)) 107 { 108 return null; 109 } 110 111 return l1 + ":" + l2; 112 } 113 114 /** 115 * Merge inherited and base markup. 116 * 117 * @param markup 118 * The inherited markup 119 * @param baseMarkup 120 * The base markup 121 * @param extendIndex 122 * Index where <wicket:extend> has been found 123 */ 124 private void merge(final IMarkupFragment markup, final IMarkupFragment baseMarkup, 125 int extendIndex) 126 { 127 // True if either <wicket:head> or <head> has been processed 128 boolean wicketHeadProcessed = false; 129 130 // True, if <head> was found 131 boolean foundHeadTag = false; 132 133 // Add all elements from the base markup to the new list 134 // until <wicket:child/> is found. Convert <wicket:child/> 135 // into <wicket:child> and add it as well. 136 WicketTag childTag = null; 137 int baseIndex = 0; 138 MarkupResourceStream markupResourceStream = baseMarkup.getMarkupResourceStream(); 139 IResourceStream resource = markupResourceStream.getResource(); 140 Class<? extends Component> markupClass = markupResourceStream.getMarkupClass(); 141 142 for (; baseIndex < baseMarkup.size(); baseIndex++) 143 { 144 MarkupElement element = baseMarkup.get(baseIndex); 145 if (element instanceof RawMarkup) 146 { 147 // Add the element to the merged list 148 addMarkupElement(element); 149 continue; 150 } 151 152 final ComponentTag tag = (ComponentTag)element; 153 154 // Make sure all tags of the base markup remember where they are 155 // from 156 if (resource != null && tag.getMarkupClass() == null) 157 { 158 tag.setMarkupClass(markupClass); 159 } 160 161 if (element instanceof WicketTag) 162 { 163 WicketTag wtag = (WicketTag)element; 164 165 // Found wicket:child in the base markup. In case of 3+ 166 // level inheritance make sure the child tag is not from one of 167 // the deeper levels 168 if (wtag.isChildTag() && tag.getMarkupClass() == markupClass) 169 { 170 if (wtag.isOpenClose()) 171 { 172 // <wicket:child /> => <wicket:child>...</wicket:child> 173 childTag = wtag; 174 WicketTag childOpenTag = (WicketTag)wtag.mutable(); 175 childOpenTag.getXmlTag().setType(TagType.OPEN); 176 childOpenTag.setMarkupClass(markupClass); 177 addMarkupElement(childOpenTag); 178 break; 179 } 180 else if (wtag.isOpen()) 181 { 182 // <wicket:child> 183 addMarkupElement(wtag); 184 break; 185 } 186 else 187 { 188 throw new WicketRuntimeException( 189 "Did not expect a </wicket:child> tag in " + baseMarkup.toString()); 190 } 191 } 192 193 // Process the head of the extended markup only once 194 if (wicketHeadProcessed == false) 195 { 196 // if </wicket:head> in base markup and no <head> 197 if (wtag.isClose() && wtag.isHeadTag() && (foundHeadTag == false)) 198 { 199 wicketHeadProcessed = true; 200 201 // Add the current close tag 202 addMarkupElement(wtag); 203 204 // Add the <wicket:head> body from the derived markup. 205 copyWicketHead(markup, extendIndex); 206 207 // Do not add the current tag. It has already been added. 208 continue; 209 } 210 211 // if <wicket:panel> or ... in base markup 212 if (wtag.isOpen() && wtag.isMajorWicketComponentTag()) 213 { 214 wicketHeadProcessed = true; 215 216 // Add the <wicket:head> body from the derived markup. 217 copyWicketHead(markup, extendIndex); 218 } 219 } 220 } 221 222 // Process the head of the extended markup only once 223 if (wicketHeadProcessed == false) 224 { 225 // Remember that we found <head> in the base markup 226 if (tag.isOpen() && TagUtils.isHeadTag(tag)) 227 { 228 foundHeadTag = true; 229 } 230 231 // if <head> in base markup 232 if ((tag.isClose() && TagUtils.isHeadTag(tag)) || 233 (tag.isClose() && TagUtils.isWicketHeaderItemsTag(tag)) || 234 (tag.isOpen() && TagUtils.isBodyTag(tag))) 235 { 236 wicketHeadProcessed = true; 237 238 // Add the <wicket:head> body from the derived markup. 239 copyWicketHead(markup, extendIndex); 240 } 241 } 242 243 // Add the element to the merged list 244 addMarkupElement(element); 245 } 246 247 if (baseIndex == baseMarkup.size()) 248 { 249 throw new WicketRuntimeException("Expected to find <wicket:child/> in base markup: " + 250 baseMarkup.toString()); 251 } 252 253 // Now append all elements from the derived markup starting with 254 // <wicket:extend> until </wicket:extend> to the list 255 for (; extendIndex < markup.size(); extendIndex++) 256 { 257 MarkupElement element = markup.get(extendIndex); 258 addMarkupElement(element); 259 260 if (element instanceof WicketTag) 261 { 262 WicketTag wtag = (WicketTag)element; 263 if (wtag.isExtendTag() && wtag.isClose()) 264 { 265 break; 266 } 267 } 268 } 269 270 if (extendIndex == markup.size()) 271 { 272 throw new WicketRuntimeException( 273 "Missing close tag </wicket:extend> in derived markup: " + markup.toString()); 274 } 275 276 // If <wicket:child> than skip the body and find </wicket:child> 277 if (((ComponentTag)baseMarkup.get(baseIndex)).isOpen()) 278 { 279 for (baseIndex++; baseIndex < baseMarkup.size(); baseIndex++) 280 { 281 MarkupElement element = baseMarkup.get(baseIndex); 282 if (element instanceof WicketTag) 283 { 284 WicketTag tag = (WicketTag)element; 285 if (tag.isChildTag() && tag.isClose()) 286 { 287 // Ok, skipped the childs content 288 tag.setMarkupClass(markupClass); 289 addMarkupElement(tag); 290 break; 291 } 292 else 293 { 294 throw new WicketRuntimeException( 295 "Wicket tags like <wicket:xxx> are not allowed in between <wicket:child> and </wicket:child> tags: " + 296 markup.toString()); 297 } 298 } 299 else if (element instanceof ComponentTag) 300 { 301 throw new WicketRuntimeException( 302 "Wicket tags identified by wicket:id are not allowed in between <wicket:child> and </wicket:child> tags: " + 303 markup.toString()); 304 } 305 } 306 307 // </wicket:child> not found 308 if (baseIndex == baseMarkup.size()) 309 { 310 throw new WicketRuntimeException( 311 "Expected to find </wicket:child> in base markup: " + baseMarkup.toString()); 312 } 313 } 314 else 315 { 316 // And now all remaining elements from the derived markup. 317 // But first add </wicket:child> 318 WicketTag childCloseTag = (WicketTag)childTag.mutable(); 319 childCloseTag.getXmlTag().setType(TagType.CLOSE); 320 childCloseTag.setMarkupClass(markupClass); 321 childCloseTag.setOpenTag(childTag); 322 addMarkupElement(childCloseTag); 323 } 324 325 for (baseIndex++; baseIndex < baseMarkup.size(); baseIndex++) 326 { 327 MarkupElement element = baseMarkup.get(baseIndex); 328 addMarkupElement(element); 329 330 // Make sure all tags of the base markup remember where they are 331 // from 332 if (element instanceof ComponentTag && resource != null) 333 { 334 ComponentTag tag = (ComponentTag)element; 335 if (tag.getMarkupClass() == null){ 336 tag.setMarkupClass(markupClass); 337 } 338 } 339 } 340 341 // Automatically add <head> if missing and required. On a Page 342 // it must enclose ALL of the <wicket:head> tags. 343 // Note: HtmlHeaderSectionHandler does something similar, but because 344 // markup filters are not called for merged markup again, ... 345 if (Page.class.isAssignableFrom(markup.getMarkupResourceStream().getMarkupClass())) 346 { 347 // Find the position inside the markup for first <wicket:head>, 348 // last </wicket:head> and <head> 349 int hasOpenWicketHead = -1; 350 int hasCloseWicketHead = -1; 351 int hasHead = -1; 352 for (int i = 0; i < size(); i++) 353 { 354 MarkupElement element = get(i); 355 356 boolean isHeadTag = (element instanceof WicketTag) && ((WicketTag) element).isHeadTag(); 357 if ((hasOpenWicketHead == -1) && isHeadTag) 358 { 359 hasOpenWicketHead = i; 360 } 361 else if (isHeadTag && ((ComponentTag)element).isClose()) 362 { 363 hasCloseWicketHead = i; 364 } 365 else if ((hasHead == -1) && (element instanceof ComponentTag) && 366 TagUtils.isHeadTag(element)) 367 { 368 hasHead = i; 369 } 370 else if ((hasHead != -1) && (hasOpenWicketHead != -1)) 371 { 372 break; 373 } 374 } 375 376 // If a <head> tag is missing, insert it automatically 377 if ((hasOpenWicketHead != -1) && (hasHead == -1)) 378 { 379 final XmlTag headOpenTag = new XmlTag(); 380 headOpenTag.setName("head"); 381 headOpenTag.setType(TagType.OPEN); 382 final ComponentTag openTag = new ComponentTag(headOpenTag); 383 openTag.setId(HtmlHeaderSectionHandler.HEADER_ID); 384 openTag.setAutoComponentTag(true); 385 386 final XmlTag headCloseTag = new XmlTag(); 387 headCloseTag.setName(headOpenTag.getName()); 388 headCloseTag.setType(TagType.CLOSE); 389 final ComponentTag closeTag = new ComponentTag(headCloseTag); 390 closeTag.setOpenTag(openTag); 391 closeTag.setId(HtmlHeaderSectionHandler.HEADER_ID); 392 393 addMarkupElement(hasOpenWicketHead, openTag); 394 addMarkupElement(hasCloseWicketHead + 2, closeTag); 395 } 396 } 397 } 398 399 /** 400 * Append the wicket:head regions from the extended markup to the current markup 401 * 402 * @param markup 403 * The markup of the child/inherited container 404 * @param extendIndex 405 */ 406 private void copyWicketHead(final IMarkupFragment markup, int extendIndex) 407 { 408 boolean copy = false; 409 for (int i = 0; i < extendIndex; i++) 410 { 411 MarkupElement elem = markup.get(i); 412 if (elem instanceof WicketTag) 413 { 414 WicketTag etag = (WicketTag)elem; 415 if (etag.isHeadTag()) 416 { 417 if (etag.isOpen()) 418 { 419 copy = true; 420 } 421 else 422 { 423 addMarkupElement(elem); 424 break; 425 } 426 } 427 } 428 429 if (copy) 430 { 431 addMarkupElement(elem); 432 } 433 } 434 } 435}