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.internal;
018
019import java.util.Iterator;
020
021import org.apache.wicket.Component;
022import org.apache.wicket.MarkupContainer;
023import org.apache.wicket.WicketRuntimeException;
024import org.apache.wicket.markup.ComponentTag;
025import org.apache.wicket.markup.MarkupException;
026import org.apache.wicket.markup.MarkupStream;
027import org.apache.wicket.markup.html.TransparentWebMarkupContainer;
028import org.apache.wicket.markup.html.WebMarkupContainer;
029import org.apache.wicket.markup.html.basic.EnclosureContainer;
030import org.apache.wicket.markup.parser.filter.EnclosureHandler;
031import org.apache.wicket.markup.resolver.ComponentResolvers;
032import org.apache.wicket.markup.resolver.ComponentResolvers.ResolverFilter;
033import org.apache.wicket.markup.resolver.IComponentResolver;
034import org.apache.wicket.util.string.Strings;
035
036
037/**
038 * An Enclosure are automatically created by Wicket. Do not create it yourself. An Enclosure
039 * container is created if <wicket:enclosure> is found in the markup. It is meant to solve the
040 * following situation. Instead of
041 * 
042 * <pre>
043 *    &lt;table wicket:id=&quot;label-container&quot; class=&quot;notify&quot;&gt;&lt;tr&gt;&lt;td&gt;&lt;span wicket:id=&quot;label&quot;&gt;[[notification]]&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
044 *    WebMarkupContainer container=new WebMarkupContainer(&quot;label-container&quot;)
045 *    {
046 *       public boolean isVisible()
047 *       {
048 *           return hasNotification();
049 *       }
050 *    };
051 *    add(container);
052 *     container.add(new Label(&quot;label&quot;, notificationModel));
053 * </pre>
054 * 
055 * with Enclosure you are able to do the following:
056 * 
057 * <pre>
058 *    &lt;wicket:enclosure&gt;
059 *      &lt;table class=&quot;notify&quot;&gt;&lt;tr&gt;&lt;td&gt;&lt;span wicket:id=&quot;label&quot;&gt;[[notification]]&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
060 *    &lt;/wicket:enclosure&gt;
061 *    add(new Label(&quot;label&quot;, notificationModel))
062 *    {
063 *       public boolean isVisible()
064 *       {
065 *           return hasNotification();
066 *       }
067 *    }
068 * </pre>
069 * <p>
070 * Please note that since a transparent auto-component is created for the tag, the markup and the
071 * component hierarchy will not be in sync which leads to subtle differences if your code relies on
072 * onBeforeRender() and validate() being called for the children inside the enclosure tag. E.g. it
073 * might happen that onBeforeRender() and validate() gets called on invisible components. In doubt,
074 * please fall back to {@link EnclosureContainer}.
075 * </p>
076 * <p>
077 * Additionally due to the reason above it is not possible to assert that children in Enclosure are
078 * not visible to WicketTester.
079 * </p>
080 * 
081 * @see EnclosureHandler
082 * @see EnclosureContainer
083 * 
084 * @author igor
085 * @author Juergen Donnerstag
086 * @since 1.3
087 */
088public class Enclosure extends WebMarkupContainer implements IComponentResolver
089{
090        private static final long serialVersionUID = 1L;
091
092        /** The child component to delegate the isVisible() call to */
093        private Component childComponent;
094
095        /** Id of the child component that will control visibility of the enclosure */
096        private final String childId;
097
098        /**
099         * Construct.
100         * 
101         * @param id
102         * @param childId
103         */
104        public Enclosure(final String id, final String childId)
105        {
106                super(id);
107
108                if (childId == null)
109                {
110                        throw new MarkupException(
111                                "You most likely forgot to register the EnclosureHandler with the MarkupParserFactory");
112                }
113
114                this.childId = childId;
115        }
116
117        /**
118         * 
119         * @return child id
120         */
121        public final String getChildId()
122        {
123                return childId.toString();
124        }
125
126        protected final Component getChild()
127        {
128                if (childComponent == null)
129                {
130                        // try to find child when queued
131                        childComponent = resolveChild(this);
132                }
133                if (childComponent == null)
134                {
135                        // try to find child when resolved
136                        childComponent = getChildComponent(new MarkupStream(getMarkup()), getEnclosureParent());
137                }
138                return childComponent;
139        }
140        
141        /**
142         * Searches for the controlling child component looking also 
143         * through transparent components.
144         * 
145         * @param container
146         *                      the current container
147         * @return the controlling child component, null if no one is found 
148         */
149        private Component resolveChild(MarkupContainer container)
150        {
151                Component childController = container.get(childId);
152                
153                Iterator<Component> children = container.iterator();
154                
155                while (children.hasNext() && childController == null)
156                {
157                        Component transparentChild = children.next();
158                        
159                        if(transparentChild instanceof TransparentWebMarkupContainer)
160                        {
161                                childController = resolveChild((MarkupContainer)transparentChild);
162                        }
163                }
164                
165                return childController;
166        }
167
168        @Override
169        public boolean isVisible()
170        {
171                return getChild().determineVisibility();
172        }
173        
174        @Override
175        protected void onConfigure()
176        {
177                super.onConfigure();
178                final Component child = getChild();
179                
180                child.configure();
181                boolean childVisible = child.determineVisibility();
182                
183                setVisible(childVisible);
184        }
185
186        @Override
187        protected void onDetach()
188        {
189                super.onDetach();
190
191                // necessary when queued and lives with the page instead of just during render
192                childComponent = null;
193        }
194        
195        /**
196         * Get the real parent container
197         * 
198         * @return enclosure's parent markup container
199         */
200        protected MarkupContainer getEnclosureParent()
201        {
202                MarkupContainer parent = getParent();
203                
204                if (parent == null)
205                {
206                        throw new WicketRuntimeException(
207                                "Unable to find parent component which is not a transparent resolver");
208                }
209                return parent;
210        }
211
212        /**
213         * Resolves the child component which is the controller of this Enclosure
214         * 
215         * @param markupStream
216         *            the markup stream of this Enclosure
217         * @param enclosureParent
218         *            the non-auto parent component of this Enclosure
219         * @return The component associated with the {@linkplain #childId}
220         */
221        private Component getChildComponent(final MarkupStream markupStream,
222                MarkupContainer enclosureParent)
223        {
224                String fullChildId = getChildId();
225
226                Component controller = enclosureParent.get(fullChildId);
227                if (controller == null)
228                {
229                        int orgIndex = markupStream.getCurrentIndex();
230                        try
231                        {
232                                while (markupStream.isCurrentIndexInsideTheStream())
233                                {
234                                        markupStream.next();
235                                        if (markupStream.skipUntil(ComponentTag.class))
236                                        {
237                                                ComponentTag tag = markupStream.getTag();
238                                                if ((tag != null) && (tag.isOpen() || tag.isOpenClose()))
239                                                {
240                                                        String tagId = tag.getId();
241
242                                                        if (fullChildId.equals(tagId))
243                                                        {
244                                                                ComponentTag fullComponentTag = new ComponentTag(tag);
245                                                                fullComponentTag.setId(childId.toString());
246
247                                                                controller = ComponentResolvers.resolve(enclosureParent,
248                                                                        markupStream, fullComponentTag, new ResolverFilter()
249                                                                        {
250                                                                                @Override
251                                                                                public boolean ignoreResolver(
252                                                                                        final IComponentResolver resolver)
253                                                                                {
254                                                                                        return resolver instanceof EnclosureHandler;
255                                                                                }
256                                                                        });
257                                                                break;
258                                                        }
259                                                        else if (fullChildId.startsWith(tagId + PATH_SEPARATOR))
260                                                        {
261                                                                fullChildId = Strings.afterFirst(fullChildId, PATH_SEPARATOR);
262                                                        }
263                                                }
264                                        }
265                                }
266                        }
267                        finally
268                        {
269                                markupStream.setCurrentIndex(orgIndex);
270                        }
271                }
272
273                checkChildComponent(controller);
274                return controller;
275        }
276
277        @Override
278        public Component resolve(MarkupContainer container, MarkupStream markupStream, ComponentTag tag)
279        {
280                if (childId.equals(tag.getId()))
281                {
282                        return childComponent;
283                }
284                return getEnclosureParent().get(tag.getId());
285        }
286
287        /**
288         * 
289         * @param controller
290         */
291        private void checkChildComponent(final Component controller)
292        {
293                if (controller == null)
294                {
295                        throw new WicketRuntimeException("Could not find child with id: " + childId +
296                                " in the wicket:enclosure");
297                }
298                else if (controller == this)
299                {
300                        throw new WicketRuntimeException(
301                                "Programming error: childComponent == enclose component; endless loop");
302                }
303        }
304}