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;
018
019import java.util.ArrayDeque;
020
021import org.apache.wicket.markup.ComponentTag;
022import org.apache.wicket.markup.IMarkupFragment;
023import org.apache.wicket.markup.MarkupElement;
024
025/**
026 * Context for component dequeueing. Keeps track of markup position and container stack.
027 * 
028 * @author igor
029 *
030 */
031public final class DequeueContext
032{
033        private final IMarkupFragment markup;
034        private int index;
035        private ComponentTag next;
036        private ArrayDeque<ComponentTag> tags = new ArrayDeque<>();
037        private final boolean skipFirst;
038        private ComponentTag first;
039
040        private ArrayDeque<MarkupContainer> containers = new ArrayDeque<>();
041
042        /** A bookmark for the DequeueContext stack */
043        public static final class Bookmark
044        {
045                private final int index;
046                private final ComponentTag next;
047                private final ArrayDeque<ComponentTag> tags;
048                private final ArrayDeque<MarkupContainer> containers;
049
050                private Bookmark(DequeueContext parser)
051                {
052                        this.index = parser.index;
053                        this.next = parser.next;
054                        this.tags = new ArrayDeque<>(parser.tags);
055                        this.containers = new ArrayDeque<>(parser.containers);
056                }
057
058                private void restore(DequeueContext parser)
059                {
060                        parser.index = index;
061                        parser.next = next;
062                        parser.tags = new ArrayDeque<>(tags);
063                        parser.containers = new ArrayDeque<>(containers);
064                }
065        }
066
067        public DequeueContext(IMarkupFragment markup, MarkupContainer root, boolean skipFirst)
068        {
069                this.markup = markup;
070                this.skipFirst = skipFirst;
071                this.containers.push(root);
072                this.next = nextTag();
073        }
074
075        /**
076         * Saves the state of the context into a bookmark which can later be used to restore it.
077         */
078        public Bookmark save()
079        {
080                return new Bookmark(this);
081        }
082
083        /**
084         * Restores the state of the context from the bookmark
085         * 
086         * @param bookmark
087         */
088        public void restore(Bookmark bookmark)
089        {
090                bookmark.restore(this);
091        }
092
093        /**
094         * Peeks markup tag that would be retrieved by call to {@link #takeTag()}
095         * 
096         * @return
097         */
098        public ComponentTag peekTag()
099        {
100                return next;
101        }
102
103        /**
104         * Retrieves the next markup tag
105         * 
106         * @return
107         */
108        public ComponentTag takeTag()
109        {
110                ComponentTag taken = next;
111
112                if (taken == null)
113                {
114                        return null;
115                }
116
117                if (taken.isOpen() && !taken.hasNoCloseTag())
118                {
119                        tags.push(taken);
120                }
121                else if (tags.size() > 0 && taken.closes(tags.peek()))
122                {
123                        tags.pop();
124                }
125                next = nextTag();
126                return taken;
127        }
128
129        /**
130         * Skips to the closing tag of the tag retrieved from last call to {@link #takeTag()}
131         */
132        public void skipToCloseTag()
133        {
134                while (!next.closes(tags.peek()))
135                {
136                        next = nextTag();
137                }
138        }
139
140        private ComponentTag nextTag()
141        {
142                if (skipFirst && first == null)
143                {
144                        for (; index < markup.size(); index++)
145                        {
146                                MarkupElement element = markup.get(index);
147                                if (element instanceof ComponentTag)
148                                {
149                                        first = (ComponentTag)element;
150                                        index++;
151                                        break;
152                                }
153                        }
154                }
155
156                for (; index < markup.size(); index++)
157                {
158                        MarkupElement element = markup.get(index);
159                        if (element instanceof ComponentTag)
160                        {
161                                ComponentTag tag = (ComponentTag)element;
162
163                                if (tag.isOpen() || tag.isOpenClose())
164                                {
165                                        DequeueTagAction action = canDequeueTag(tag);
166                                        switch (action)
167                                        {
168                                                case IGNORE :
169                                                        continue;
170                                                case DEQUEUE :
171                                                        index++;
172                                                        return tag;
173                                                case SKIP : // skip to close tag
174                                                        boolean found = false;
175                                                        for (; index < markup.size(); index++)
176                                                        {
177                                                                if ((markup.get(index) instanceof ComponentTag)
178                                                                        && markup.get(index).closes(tag))
179                                                                {
180                                                                        found = true;
181                                                                        break;
182                                                                }
183                                                        }
184                                                        if (!found)
185                                                        {
186                                                                throw new IllegalStateException(String.format(
187                                                                        "Could not find close tag for tag '%s' in markup: %s ", tag,
188                                                                        markup));
189                                                        }
190
191                                        }
192                                }
193                                else
194                                {
195                                        // closed tag
196                                        ComponentTag open = tag.isClose() ? tag.getOpenTag() : tag;
197
198                                        if (skipFirst && first != null && open == first)
199                                        {
200                                                continue;
201                                        }
202
203                                        switch (canDequeueTag(open))
204                                        {
205                                                case DEQUEUE :
206                                                        index++;
207                                                        return tag;
208                                                case IGNORE :
209                                                        continue;
210                                                case SKIP :
211                                                        throw new IllegalStateException(String.format(
212                                                                "Should not see closed tag of skipped open tag '%s' in markup:%s",
213                                                                tag, markup));
214                                        }
215                                }
216                        }
217                }
218                return null;
219        }
220
221        private DequeueTagAction canDequeueTag(ComponentTag open)
222        {
223                if (containers.size() < 1)
224                {
225                        // TODO queueing message: called too early
226                        throw new IllegalStateException();
227                }
228
229                DequeueTagAction action;
230                for (MarkupContainer container : containers)
231                {
232                        action = container.canDequeueTag(open);
233                        if (action != null)
234                        {
235                                return action;
236                        }
237                }
238                return DequeueTagAction.IGNORE;
239        }
240
241        /**
242         * Checks if the tag returned by {@link #peekTag()} is either open or open-close.
243         * 
244         * @return
245         */
246        public boolean isAtOpenOrOpenCloseTag()
247        {
248                ComponentTag tag = peekTag();
249                return tag != null && (tag.isOpen() || tag.isOpenClose());
250        }
251
252        /**
253         * Retrieves the container on the top of the containers stack
254         * 
255         * @return
256         */
257        public MarkupContainer peekContainer()
258        {
259                return containers.peek();
260        }
261
262        /**
263         * Pushes a container onto the container stack
264         * 
265         * @param container
266         */
267        public void pushContainer(MarkupContainer container)
268        {
269                containers.push(container);
270        }
271
272        /**
273         * Pops a container from the container stack
274         * 
275         * @return
276         */
277        public MarkupContainer popContainer()
278        {
279                return containers.pop();
280        }
281
282        /**
283         * Searches the container stack for a component that can be dequeude
284         * 
285         * @param tag
286         * @return
287         */
288        public Component findComponentToDequeue(ComponentTag tag)
289        {
290                for (MarkupContainer container : containers)
291                {
292                        Component child = container.findComponentToDequeue(tag);
293                        if (child != null)
294                        {
295                                return child;
296                        }
297                }
298                return null;
299        }
300
301}