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 &lt;wicket:child/&gt; tag at the position where the derived markup should
041 * be inserted. From the derived markup all tags in between &lt;wicket:extend&gt; and
042 * &lt;/wicket:extend&gt; will be inserted.
043 * <p>
044 * In addition, all &lt;wicket:head&gt; regions are copied as well. This allows to develop completely
045 * self-contained plug &amp; 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 &lt;wicket:extend&gt; 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}