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         * &lt;wicket:head&gt; 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}