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.parser.filter;
018
019import java.text.ParseException;
020import java.util.ArrayDeque;
021import java.util.Deque;
022import java.util.Iterator;
023
024import org.apache.wicket.Component;
025import org.apache.wicket.MarkupContainer;
026import org.apache.wicket.markup.ComponentTag;
027import org.apache.wicket.markup.MarkupElement;
028import org.apache.wicket.markup.MarkupResourceStream;
029import org.apache.wicket.markup.MarkupStream;
030import org.apache.wicket.markup.WicketTag;
031import org.apache.wicket.markup.html.internal.InlineEnclosure;
032import org.apache.wicket.markup.parser.AbstractMarkupFilter;
033import org.apache.wicket.markup.resolver.IComponentResolver;
034import org.apache.wicket.util.string.Strings;
035
036
037/**
038 * This is a markup inline filter. It identifies enclosures as attribute, for example: <tr
039 * wicket:enclosure="">. The <tr> tag used in the example can be replaced with any html tag
040 * that can contain child elements. If the 'child' attribute is empty it determines the wicket:id of
041 * the child component automatically by analyzing the wicket component (in this case one wicket
042 * component is allowed) in between the open and close tags. If the enclosure tag has a 'child'
043 * attribute like <code>&lt;tr
044 * wicket:enclosure="controllingChildId"&gt;</code> than more than just one wicket component inside
045 * the enclosure tags are allowed and the child component which determines the visibility of the
046 * enclosure is identified by the 'child' attribute value which must be equal to the relative child
047 * id path.
048 * 
049 * @see InlineEnclosure
050 * 
051 * @author Joonas Hamalainen
052 * @author Juergen Donnerstag
053 */
054public final class InlineEnclosureHandler extends AbstractMarkupFilter
055        implements
056                IComponentResolver
057{
058        private static final long serialVersionUID = 1L;
059
060        /** The Component id prefix. */
061        public final static String INLINE_ENCLOSURE_ID_PREFIX = "InlineEnclosure-";
062
063        /** Attribute to identify inline enclosures */
064        public final static String INLINE_ENCLOSURE_ATTRIBUTE_NAME = "enclosure";
065
066        /** enclosures inside enclosures */
067        private Deque<ComponentTag> enclosures;
068
069        /**
070         * Construct.
071         */
072        public InlineEnclosureHandler()
073        {
074                this(null);
075        }
076
077        public InlineEnclosureHandler(MarkupResourceStream resourceStream)
078        {
079                super(resourceStream);
080        }
081
082        @Override
083        protected MarkupElement onComponentTag(final ComponentTag tag) throws ParseException
084        {
085                // We only need ComponentTags
086                if (tag instanceof WicketTag)
087                {
088                        return tag;
089                }
090
091                // Has wicket:enclosure attribute?
092                String enclosureAttr = getAttribute(tag, null);
093                if (enclosureAttr != null)
094                {
095                        if (tag.isOpen())
096                        {
097                                // Make sure 'wicket:id' and 'id' are consistent
098                                String htmlId = tag.getAttribute("id");
099                                if ((tag.getId() != null) && !Strings.isEmpty(htmlId) &&
100                                        !htmlId.equals(tag.getId()))
101                                {
102                                        throw new ParseException(
103                                                "Make sure that 'id' and 'wicket:id' are the same if both are provided. Tag:" +
104                                                        tag.toString(), tag.getPos());
105                                }
106
107                                // if it doesn't have a wicket-id already, then assign one now.
108                                if (Strings.isEmpty(tag.getId()))
109                                {
110                                        if (Strings.isEmpty(htmlId))
111                                        {
112                                                String id = getWicketNamespace() + "_" + INLINE_ENCLOSURE_ID_PREFIX + 
113                                                        getRequestUniqueId();
114                                                tag.setId(id);
115                                        }
116                                        else
117                                        {
118                                                tag.setId(htmlId);
119                                        }
120
121                                        tag.setAutoComponentTag(true);
122                                        tag.setAutoComponentFactory(new ComponentTag.IAutoComponentFactory()
123                                        {
124                                                @Override
125                                                public Component newComponent(MarkupContainer container, ComponentTag tag)
126                                                {
127                                                        String attributeName = getInlineEnclosureAttributeName(null);
128                                                        String childId = tag.getAttribute(attributeName);
129                                                        return new InlineEnclosure(tag.getId(), childId);
130                                                }
131                                        });
132                                        tag.setModified(true);
133                                }
134
135                                // Put the enclosure on the stack. The most current one will be on top
136                                if (enclosures == null)
137                                {
138                                        enclosures = new ArrayDeque<>();
139                                }
140                                enclosures.push(tag);
141                        }
142                        else
143                        {
144                                throw new ParseException(
145                                        "Open-close tags don't make sense for InlineEnclosure. Tag:" + tag.toString(),
146                                        tag.getPos());
147                        }
148                }
149                // Are we within an enclosure?
150                else if ((enclosures != null) && (enclosures.size() > 0))
151                {
152                        // In case the enclosure tag did not provide a child component id, then assign the
153                        // first ComponentTag's id found as the controlling child to the enclosure.
154                        if (tag.isOpen() && (tag.getId() != null) && !(tag instanceof WicketTag) &&
155                                !tag.isAutoComponentTag())
156                        {
157                                Iterator<ComponentTag> componentTagIterator = enclosures.descendingIterator();
158                                while (componentTagIterator.hasNext())
159                                {
160                                        ComponentTag lastEnclosure = componentTagIterator.next();
161                                        String attr = getAttribute(lastEnclosure, null);
162                                        if (Strings.isEmpty(attr) == true)
163                                        {
164                                                lastEnclosure.getAttributes().put(getInlineEnclosureAttributeName(null),
165                                                        tag.getId());
166                                                lastEnclosure.setModified(true);
167                                        }
168                                }
169                        }
170                        else if (tag.isClose() && tag.closes(enclosures.peek()))
171                        {
172                                ComponentTag lastEnclosure = enclosures.pop();
173                                String attr = getAttribute(lastEnclosure, null);
174                                if (Strings.isEmpty(attr) == true)
175                                {
176                                        throw new ParseException("Did not find any child for InlineEnclosure. Tag:" +
177                                                lastEnclosure.toString(), tag.getPos());
178                                }
179                        }
180                }
181
182                return tag;
183        }
184
185        /**
186         * @param tag
187         *      The ComponentTag of the markup element with wicket:enclosure attribute
188         * @return The value of wicket:enclosure attribute or null if not found
189         */
190        private String getAttribute(final ComponentTag tag, MarkupStream markupStream)
191        {
192                return tag.getAttributes().getString(getInlineEnclosureAttributeName(markupStream));
193        }
194
195        @Override
196        public Component resolve(final MarkupContainer container, final MarkupStream markupStream,
197                final ComponentTag tag)
198        {
199                String inlineEnclosureChildId = getAttribute(tag, markupStream);
200                if (Strings.isEmpty(inlineEnclosureChildId) == false)
201                {
202                        String id = tag.getId();
203
204                        // Yes, we handled the tag
205                        return new InlineEnclosure(id, inlineEnclosureChildId);
206                }
207
208                // We were not able to handle the tag
209                return null;
210        }
211
212        private String getInlineEnclosureAttributeName(MarkupStream markupStream) {
213                return getWicketNamespace(markupStream) + ':' + INLINE_ENCLOSURE_ATTRIBUTE_NAME;
214        }
215
216}