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.util.lang.Args;
021import org.apache.wicket.util.resource.IResourceStream;
022import org.apache.wicket.util.string.Strings;
023
024
025/**
026 * A stream of {@link org.apache.wicket.markup.MarkupElement}s, subclasses of which are
027 * {@link org.apache.wicket.markup.ComponentTag} and {@link org.apache.wicket.markup.RawMarkup}. A
028 * markup stream has a current index in the list of markup elements. The next markup element can be
029 * retrieved and the index advanced by calling next(). If the index hits the end, hasMore() will
030 * return false.
031 * <p>
032 * The current markup element can be accessed with get() and as a ComponentTag with getTag().
033 * <p>
034 * The stream can be sought to a particular location with setCurrentIndex().
035 * <p>
036 * Convenience methods also exist to skip component tags (and any potentially nested markup) or raw
037 * markup.
038 * <p>
039 * Several boolean methods of the form at*() return true if the markup stream is positioned at a tag
040 * with a given set of characteristics.
041 * <p>
042 * The resource from which the markup was loaded can be retrieved with getResource().
043 * 
044 * @author Jonathan Locke
045 */
046public class MarkupStream
047{
048        /** Element at currentIndex */
049        private MarkupElement current;
050
051        /** Current index in markup stream */
052        private int currentIndex = 0;
053
054        /** The markup element list */
055        private final IMarkupFragment markup;
056
057        /**
058         * Constructor
059         * 
060         * @param markup
061         *            List of markup elements
062         */
063        public MarkupStream(final IMarkupFragment markup)
064        {
065                Args.notNull(markup, "markup");
066
067                this.markup = markup;
068
069                if (markup.size() > 0)
070                {
071                        current = get(currentIndex);
072                }
073        }
074
075        /**
076         * @return True if current markup element is a close tag
077         */
078        public boolean atCloseTag()
079        {
080                return atTag() && getTag().isClose();
081        }
082
083        /**
084         * @return True if current markup element is an openclose tag
085         */
086        public boolean atOpenCloseTag()
087        {
088                return atTag() && getTag().isOpenClose();
089        }
090
091        /**
092         * @param componentId
093         *            Required component name attribute
094         * @return True if the current markup element is an openclose tag with the given component name
095         */
096        public boolean atOpenCloseTag(final String componentId)
097        {
098                return atOpenCloseTag() && componentId.equals(getTag().getId());
099        }
100
101        /**
102         * @return True if current markup element is an open tag
103         */
104        public boolean atOpenTag()
105        {
106                return atTag() && getTag().isOpen();
107        }
108
109        /**
110         * @param id
111         *            Required component id attribute
112         * @return True if the current markup element is an open tag with the given component name
113         */
114        public boolean atOpenTag(final String id)
115        {
116                return atOpenTag() && id.equals(getTag().getId());
117        }
118
119        /**
120         * @return True if current markup element is a tag
121         */
122        public boolean atTag()
123        {
124                return current instanceof ComponentTag;
125        }
126
127        /**
128         * Compare this markup stream with another one
129         * 
130         * @param that
131         *            The other markup stream
132         * @return True if each MarkupElement in this matches each element in that
133         */
134        public boolean equalTo(final MarkupStream that)
135        {
136                // While a has more markup elements
137                while (isCurrentIndexInsideTheStream())
138                {
139                        // Get an element from each
140                        final MarkupElement thisElement = this.get();
141                        final MarkupElement thatElement = that.get();
142
143                        // and if the elements are not equal
144                        if (thisElement != null && thatElement != null)
145                        {
146                                if (!thisElement.equalTo(thatElement))
147                                {
148                                        // fail the comparison
149                                        return false;
150                                }
151                        }
152                        else
153                        {
154                                // If one element is null,
155                                if (!(thisElement == null && thatElement == null))
156                                {
157                                        // fail the comparison
158                                        return false;
159                                }
160                        }
161                        next();
162                        that.next();
163                }
164
165                // If we've run out of markup elements in b
166                if (!that.isCurrentIndexInsideTheStream())
167                {
168                        // then the two streams match perfectly
169                        return true;
170                }
171
172                // Stream b had extra elements
173                return false;
174        }
175
176        /**
177         * True, if associate markup is the same. It will change e.g. if the markup file has been
178         * re-loaded or the locale has been changed.
179         * 
180         * @param markupStream
181         *            The markup stream to compare with.
182         * @return true, if markup has not changed
183         */
184        public final boolean equalMarkup(final MarkupStream markupStream)
185        {
186                if (markupStream == null)
187                {
188                        return false;
189                }
190                return markup == markupStream.markup;
191        }
192
193        /**
194         * @return The current markup element
195         */
196        public MarkupElement get()
197        {
198                return current;
199        }
200
201        /**
202         * @param index
203         *            The index of a markup element
204         * @return The MarkupElement element
205         */
206        public MarkupElement get(final int index)
207        {
208                return markup.get(index);
209        }
210
211        /**
212         * Get the component/container's Class which is directly associated with the stream.
213         * 
214         * @return The component's class
215         */
216        public final Class<? extends Component> getContainerClass()
217        {
218                return markup.getMarkupResourceStream().getMarkupClass();
219        }
220
221        /**
222         * @return Current index in markup stream
223         */
224        public int getCurrentIndex()
225        {
226                return currentIndex;
227        }
228
229        /**
230         * Gets the markup encoding. A markup encoding may be specified in a markup file with an XML
231         * encoding specifier of the form &lt;?xml ... encoding="..." ?&gt;.
232         * 
233         * @return The encoding, or null if not found
234         */
235        public final String getEncoding()
236        {
237                return markup.getMarkupResourceStream().getEncoding();
238        }
239
240        /**
241         * @return The resource where this markup stream came from
242         */
243        public IResourceStream getResource()
244        {
245                return markup.getMarkupResourceStream().getResource();
246        }
247
248        /**
249         * @return The current markup element as a markup tag
250         */
251        public ComponentTag getTag()
252        {
253                if (current instanceof ComponentTag)
254                {
255                        return (ComponentTag)current;
256                }
257
258                throwMarkupException("Tag expected");
259
260                return null;
261        }
262
263        /**
264         * Get the wicket namespace valid for this specific markup
265         * 
266         * @return wicket namespace
267         */
268        public final String getWicketNamespace()
269        {
270                return markup.getMarkupResourceStream().getWicketNamespace();
271        }
272
273        /**
274         * @return True if this markup stream is moved to a MarkupElement element
275         */
276        public boolean isCurrentIndexInsideTheStream()
277        {
278                return currentIndex < markup.size();
279        }
280
281        /**
282         * @return True if this markup stream has more MarkupElement elements
283         */
284        public boolean hasMore()
285        {
286                return currentIndex < (markup.size() - 1);
287        }
288
289        /**
290         * 
291         * @return true, if underlying markup has been merged (inheritance)
292         */
293        public final boolean isMergedMarkup()
294        {
295                return markup instanceof MergedMarkup;
296        }
297
298        /**
299         * Note:
300         * 
301         * @return The next markup element in the stream
302         */
303        public MarkupElement next()
304        {
305                if (++currentIndex < markup.size())
306                {
307                        return current = get(currentIndex);
308                }
309
310                return null;
311        }
312
313        /**
314         * Note:
315         * 
316         * @return The next markup element in the stream
317         */
318        public MarkupElement nextOpenTag()
319        {
320                while (next() != null)
321                {
322                        MarkupElement elem = get();
323                        if (elem instanceof ComponentTag)
324                        {
325                                ComponentTag tag = (ComponentTag)elem;
326                                if (tag.isOpen() || tag.isOpenClose())
327                                {
328                                        return current = get(currentIndex);
329                                }
330                        }
331                }
332
333                return null;
334        }
335
336        /**
337         * @param currentIndex
338         *            New current index in the stream
339         * @return this
340         */
341        public MarkupStream setCurrentIndex(final int currentIndex)
342        {
343                current = get(currentIndex);
344                this.currentIndex = currentIndex;
345                return this;
346        }
347
348        /**
349         * Skips this component and all nested components
350         */
351        public final void skipComponent()
352        {
353                // Get start tag
354                final ComponentTag startTag = getTag();
355
356                if (startTag.isOpen())
357                {
358                        // With HTML not all tags require a close tag which
359                        // must have been detected by the HtmlHandler earlier on.
360                        if (startTag.hasNoCloseTag() == false)
361                        {
362                                // Skip <tag>
363                                next();
364
365                                // Skip nested components
366                                skipToMatchingCloseTag(startTag);
367                        }
368
369                        // Skip </tag>
370                        next();
371                }
372                else if (startTag.isOpenClose())
373                {
374                        // Skip <tag/>
375                        next();
376                }
377                else
378                {
379                        // We were something other than <tag> or <tag/>
380                        throwMarkupException("Skip component called on bad markup element " + startTag);
381                }
382        }
383
384        /**
385         * Skips any raw markup at the current position
386         */
387        public void skipRawMarkup()
388        {
389                while (true)
390                {
391                        if (current instanceof RawMarkup)
392                        {
393                                if (next() != null)
394                                {
395                                        continue;
396                                }
397                        }
398                        else if ((current instanceof ComponentTag) && !(current instanceof WicketTag))
399                        {
400                                ComponentTag tag = (ComponentTag)current;
401                                if (tag.isAutoComponentTag())
402                                {
403                                        if (next() != null)
404                                        {
405                                                continue;
406                                        }
407                                }
408                                else if (tag.isClose() && tag.getOpenTag().isAutoComponentTag())
409                                {
410                                        if (next() != null)
411                                        {
412                                                continue;
413                                        }
414                                }
415                        }
416                        break;
417                }
418        }
419
420        /**
421         * Skip until an element of type 'clazz' is found
422         * 
423         * @param clazz
424         * @return true if found
425         */
426        public boolean skipUntil(final Class<? extends MarkupElement> clazz)
427        {
428                while (isCurrentIndexInsideTheStream())
429                {
430                        if (clazz.isInstance(current))
431                        {
432                                return true;
433                        }
434                        next();
435                }
436
437                return false;
438        }
439
440        /**
441         * Skips any markup at the current position until the wicket tag name is found.
442         * 
443         * @param wicketTagName
444         *            wicket tag name to seek
445         */
446        public void skipUntil(final String wicketTagName)
447        {
448                while (true)
449                {
450                        if ((current instanceof WicketTag) &&
451                                ((WicketTag)current).getName().equals(wicketTagName))
452                        {
453                                return;
454                        }
455
456                        // go on until we reach the end
457                        if (next() == null)
458                        {
459                                return;
460                        }
461                }
462        }
463
464        /**
465         * Renders markup until a closing tag for openTag is reached.
466         * 
467         * @param openTag
468         *            The open tag
469         */
470        public void skipToMatchingCloseTag(final ComponentTag openTag)
471        {
472                // Loop through the markup in this container
473                while (isCurrentIndexInsideTheStream())
474                {
475                        // If the current markup tag closes the openTag
476                        if (get().closes(openTag))
477                        {
478                                // Done!
479                                return;
480                        }
481
482                        // Skip element
483                        next();
484                }
485                throwMarkupException("Expected close tag for " + openTag);
486        }
487
488        /**
489         * @return A markup fragment starting at the current position
490         */
491        public final IMarkupFragment getMarkupFragment()
492        {
493                return new MarkupFragment(markup, currentIndex);
494        }
495
496        /**
497         * Gets the attribute with 'name' for the tag at the current position
498         * 
499         * @param name
500         * @param withWicketNamespace
501         * @return null, if not found
502         */
503        public final String getTagAttribute(final String name, final boolean withWicketNamespace)
504        {
505                String attr = (withWicketNamespace ? attr = getWicketNamespace() + ":" + name : name);
506                return getTag().getAttributes().getString(attr);
507        }
508
509        /**
510         * Sometime its necessary to get the previous markup element versus the current one.
511         * 
512         * @return The previous element (currentIndex - 1)
513         */
514        public final ComponentTag getPreviousTag()
515        {
516                MarkupElement elem = get(currentIndex - 1);
517                if ((elem instanceof ComponentTag) == false)
518                {
519                        throwMarkupException("Tag expected");
520                }
521
522                return (ComponentTag)elem;
523        }
524
525        /**
526         * Throws a new markup exception
527         * 
528         * @param message
529         *            The exception message
530         * @throws MarkupException
531         */
532        public void throwMarkupException(final String message)
533        {
534                throw new MarkupException(this, message);
535        }
536
537        /**
538         * @return An HTML string highlighting the current position in the markup stream
539         */
540        public String toHtmlDebugString()
541        {
542                final StringBuilder buffer = new StringBuilder();
543
544                for (int i = 0; i < markup.size(); i++)
545                {
546                        if (i == currentIndex)
547                        {
548                                buffer.append("<font color = \"red\">");
549                        }
550
551                        final MarkupElement element = markup.get(i);
552
553                        buffer.append(Strings.escapeMarkup(element.toString(), true).toString());
554
555                        if (i == currentIndex)
556                        {
557                                buffer.append("</font>");
558                        }
559                }
560
561                return buffer.toString();
562        }
563
564        /**
565         * @return String representation of markup stream
566         */
567        @Override
568        public String toString()
569        {
570                return "[markup = " + String.valueOf(markup) + ", index = " + currentIndex +
571                        ", current = " + ((current == null) ? "null" : current.toUserDebugString()) + "]";
572        }
573}