/* * 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; import org.apache.wicket.Component; import org.apache.wicket.Page; import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.markup.parser.XmlTag; import org.apache.wicket.markup.parser.XmlTag.TagType; import org.apache.wicket.markup.parser.filter.HtmlHeaderSectionHandler; import org.apache.wicket.util.resource.IResourceStream; import org.apache.wicket.util.string.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A Markup class which represents merged markup, as it is required for markup inheritance. * <p> * The Markups are merged at load time. Deep markup hierarchies are supported. Multiple inheritance * is not. * <p> * The markup resource file, which is associated with the markup, will be the resource of the * requested markup file. The base markup resources are not. * <p> * Base Markup must have a <wicket:child/> tag at the position where the derived markup should * be inserted. From the derived markup all tags in between <wicket:extend> and * </wicket:extend> will be inserted. * <p> * In addition, all <wicket:head> regions are copied as well. This allows to develop completely * self-contained plug & play components including javascript etc. * * @author Juergen Donnerstag */ public class MergedMarkup extends Markup { private final static Logger log = LoggerFactory.getLogger(MergedMarkup.class); /** * Merge inherited and base markup. * * @param markup * The inherited markup * @param baseMarkup * The base markup * @param extendIndex * Index where <wicket:extend> has been found */ public MergedMarkup(final Markup markup, final Markup baseMarkup, int extendIndex) { super(markup.getMarkupResourceStream()); getMarkupResourceStream().setBaseMarkup(baseMarkup); // Copy settings from derived markup MarkupResourceStream baseResourceStream = baseMarkup.getMarkupResourceStream(); getMarkupResourceStream().setEncoding(baseResourceStream.getEncoding()); getMarkupResourceStream().setWicketNamespace(baseResourceStream.getWicketNamespace()); if (log.isDebugEnabled()) { String derivedResource = Strings.afterLast(markup.getMarkupResourceStream() .getResource() .toString(), '/'); String baseResource = Strings.afterLast(baseMarkup.getMarkupResourceStream() .getResource() .toString(), '/'); log.debug("Merge markup: derived markup: " + derivedResource + "; base markup: " + baseResource); } // Merge derived and base markup merge(markup, baseMarkup, extendIndex); if (log.isDebugEnabled()) { log.debug("Merge markup: " + toString()); } } @Override public String locationAsString() { /* * Uses both resource locations so that if the child does not have a style and the parent * does, the location is unique to this combination (or vice versa) SEE WICKET-1507 (Jeremy * Thomerson) */ String l1 = getMarkupResourceStream().getBaseMarkup().locationAsString(); String l2 = getMarkupResourceStream().locationAsString(); if ((l1 == null) && (l2 == null)) { return null; } return l1 + ":" + l2; } /** * Merge inherited and base markup. * * @param markup * The inherited markup * @param baseMarkup * The base markup * @param extendIndex * Index where <wicket:extend> has been found */ private void merge(final IMarkupFragment markup, final IMarkupFragment baseMarkup, int extendIndex) { // True if either <wicket:head> or <head> has been processed boolean wicketHeadProcessed = false; // True, if <head> was found boolean foundHeadTag = false; // Add all elements from the base markup to the new list // until <wicket:child/> is found. Convert <wicket:child/> // into <wicket:child> and add it as well. WicketTag childTag = null; int baseIndex = 0; MarkupResourceStream markupResourceStream = baseMarkup.getMarkupResourceStream(); IResourceStream resource = markupResourceStream.getResource(); Class<? extends Component> markupClass = markupResourceStream.getMarkupClass(); for (; baseIndex < baseMarkup.size(); baseIndex++) { MarkupElement element = baseMarkup.get(baseIndex); if (element instanceof RawMarkup) { // Add the element to the merged list addMarkupElement(element); continue; } final ComponentTag tag = (ComponentTag)element; // Make sure all tags of the base markup remember where they are // from if (resource != null && tag.getMarkupClass() == null) { tag.setMarkupClass(markupClass); } if (element instanceof WicketTag) { WicketTag wtag = (WicketTag)element; // Found wicket:child in the base markup. In case of 3+ // level inheritance make sure the child tag is not from one of // the deeper levels if (wtag.isChildTag() && tag.getMarkupClass() == markupClass) { if (wtag.isOpenClose()) { // <wicket:child /> => <wicket:child>...</wicket:child> childTag = wtag; WicketTag childOpenTag = (WicketTag)wtag.mutable(); childOpenTag.getXmlTag().setType(TagType.OPEN); childOpenTag.setMarkupClass(markupClass); addMarkupElement(childOpenTag); break; } else if (wtag.isOpen()) { // <wicket:child> addMarkupElement(wtag); break; } else { throw new WicketRuntimeException( "Did not expect a </wicket:child> tag in " + baseMarkup.toString()); } } // Process the head of the extended markup only once if (wicketHeadProcessed == false) { // if </wicket:head> in base markup and no <head> if (wtag.isClose() && wtag.isHeadTag() && (foundHeadTag == false)) { wicketHeadProcessed = true; // Add the current close tag addMarkupElement(wtag); // Add the <wicket:head> body from the derived markup. copyWicketHead(markup, extendIndex); // Do not add the current tag. It has already been added. continue; } // if <wicket:panel> or ... in base markup if (wtag.isOpen() && wtag.isMajorWicketComponentTag()) { wicketHeadProcessed = true; // Add the <wicket:head> body from the derived markup. copyWicketHead(markup, extendIndex); } } } // Process the head of the extended markup only once if (wicketHeadProcessed == false) { // Remember that we found <head> in the base markup if (tag.isOpen() && TagUtils.isHeadTag(tag)) { foundHeadTag = true; } // if <head> in base markup if ((tag.isClose() && TagUtils.isHeadTag(tag)) || (tag.isClose() && TagUtils.isWicketHeaderItemsTag(tag)) || (tag.isOpen() && TagUtils.isBodyTag(tag))) { wicketHeadProcessed = true; // Add the <wicket:head> body from the derived markup. copyWicketHead(markup, extendIndex); } } // Add the element to the merged list addMarkupElement(element); } if (baseIndex == baseMarkup.size()) { throw new WicketRuntimeException("Expected to find <wicket:child/> in base markup: " + baseMarkup.toString()); } // Now append all elements from the derived markup starting with // <wicket:extend> until </wicket:extend> to the list for (; extendIndex < markup.size(); extendIndex++) { MarkupElement element = markup.get(extendIndex); addMarkupElement(element); if (element instanceof WicketTag) { WicketTag wtag = (WicketTag)element; if (wtag.isExtendTag() && wtag.isClose()) { break; } } } if (extendIndex == markup.size()) { throw new WicketRuntimeException( "Missing close tag </wicket:extend> in derived markup: " + markup.toString()); } // If <wicket:child> than skip the body and find </wicket:child> if (((ComponentTag)baseMarkup.get(baseIndex)).isOpen()) { for (baseIndex++; baseIndex < baseMarkup.size(); baseIndex++) { MarkupElement element = baseMarkup.get(baseIndex); if (element instanceof WicketTag) { WicketTag tag = (WicketTag)element; if (tag.isChildTag() && tag.isClose()) { // Ok, skipped the childs content tag.setMarkupClass(markupClass); addMarkupElement(tag); break; } else { throw new WicketRuntimeException( "Wicket tags like <wicket:xxx> are not allowed in between <wicket:child> and </wicket:child> tags: " + markup.toString()); } } else if (element instanceof ComponentTag) { throw new WicketRuntimeException( "Wicket tags identified by wicket:id are not allowed in between <wicket:child> and </wicket:child> tags: " + markup.toString()); } } // </wicket:child> not found if (baseIndex == baseMarkup.size()) { throw new WicketRuntimeException( "Expected to find </wicket:child> in base markup: " + baseMarkup.toString()); } } else { // And now all remaining elements from the derived markup. // But first add </wicket:child> WicketTag childCloseTag = (WicketTag)childTag.mutable(); childCloseTag.getXmlTag().setType(TagType.CLOSE); childCloseTag.setMarkupClass(markupClass); childCloseTag.setOpenTag(childTag); addMarkupElement(childCloseTag); } for (baseIndex++; baseIndex < baseMarkup.size(); baseIndex++) { MarkupElement element = baseMarkup.get(baseIndex); addMarkupElement(element); // Make sure all tags of the base markup remember where they are // from if (element instanceof ComponentTag && resource != null) { ComponentTag tag = (ComponentTag)element; if (tag.getMarkupClass() == null){ tag.setMarkupClass(markupClass); } } } // Automatically add <head> if missing and required. On a Page // it must enclose ALL of the <wicket:head> tags. // Note: HtmlHeaderSectionHandler does something similar, but because // markup filters are not called for merged markup again, ... if (Page.class.isAssignableFrom(markup.getMarkupResourceStream().getMarkupClass())) { // Find the position inside the markup for first <wicket:head>, // last </wicket:head> and <head> int hasOpenWicketHead = -1; int hasCloseWicketHead = -1; int hasHead = -1; for (int i = 0; i < size(); i++) { MarkupElement element = get(i); boolean isHeadTag = (element instanceof WicketTag) && ((WicketTag) element).isHeadTag(); if ((hasOpenWicketHead == -1) && isHeadTag) { hasOpenWicketHead = i; } else if (isHeadTag && ((ComponentTag)element).isClose()) { hasCloseWicketHead = i; } else if ((hasHead == -1) && (element instanceof ComponentTag) && TagUtils.isHeadTag(element)) { hasHead = i; } else if ((hasHead != -1) && (hasOpenWicketHead != -1)) { break; } } // If a <head> tag is missing, insert it automatically if ((hasOpenWicketHead != -1) && (hasHead == -1)) { final XmlTag headOpenTag = new XmlTag(); headOpenTag.setName("head"); headOpenTag.setType(TagType.OPEN); final ComponentTag openTag = new ComponentTag(headOpenTag); openTag.setId(HtmlHeaderSectionHandler.HEADER_ID); openTag.setAutoComponentTag(true); final XmlTag headCloseTag = new XmlTag(); headCloseTag.setName(headOpenTag.getName()); headCloseTag.setType(TagType.CLOSE); final ComponentTag closeTag = new ComponentTag(headCloseTag); closeTag.setOpenTag(openTag); closeTag.setId(HtmlHeaderSectionHandler.HEADER_ID); addMarkupElement(hasOpenWicketHead, openTag); addMarkupElement(hasCloseWicketHead + 2, closeTag); } } } /** * Append the wicket:head regions from the extended markup to the current markup * * @param markup * The markup of the child/inherited container * @param extendIndex */ private void copyWicketHead(final IMarkupFragment markup, int extendIndex) { boolean copy = false; for (int i = 0; i < extendIndex; i++) { MarkupElement elem = markup.get(i); if (elem instanceof WicketTag) { WicketTag etag = (WicketTag)elem; if (etag.isHeadTag()) { if (etag.isOpen()) { copy = true; } else { addMarkupElement(elem); break; } } } if (copy) { addMarkupElement(elem); } } } }