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;
018
019import java.text.ParseException;
020import java.util.ArrayDeque;
021import java.util.HashMap;
022import java.util.Iterator;
023import java.util.Locale;
024import java.util.Map;
025
026import org.apache.wicket.markup.ComponentTag;
027import org.apache.wicket.markup.WicketParseException;
028import org.apache.wicket.markup.parser.filter.HtmlHandler;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032/**
033 * Stack to push and pop HTML elements asserting its structure.
034 */
035public class TagStack
036{
037        private static final Logger log = LoggerFactory.getLogger(HtmlHandler.class);
038
039        /** Map of simple tags. */
040        private static final Map<String, Boolean> doesNotRequireCloseTag = new HashMap<String, Boolean>();
041
042        static
043        {
044                // Tags which are allowed not be closed in HTML
045                doesNotRequireCloseTag.put("p", Boolean.TRUE);
046                doesNotRequireCloseTag.put("br", Boolean.TRUE);
047                doesNotRequireCloseTag.put("img", Boolean.TRUE);
048                doesNotRequireCloseTag.put("input", Boolean.TRUE);
049                doesNotRequireCloseTag.put("hr", Boolean.TRUE);
050                doesNotRequireCloseTag.put("link", Boolean.TRUE);
051                doesNotRequireCloseTag.put("meta", Boolean.TRUE);
052        }
053
054        /** Tag stack to find balancing tags */
055        final private ArrayDeque<ComponentTag> stack = new ArrayDeque<ComponentTag>();
056        private boolean debug;
057
058        /**
059         * Assert that tag has no mismatch error. If the parameter is an open tag, just push it on stack
060         * to be tested latter.
061         * 
062         * @param tag
063         * @throws ParseException
064         */
065        public void assertValidInStack(ComponentTag tag) throws ParseException
066        {
067                // Get the next tag. If null, no more tags are available
068                if (tag == null)
069                {
070                        validate();
071                        return;
072                }
073
074                if (log.isDebugEnabled() && debug)
075                {
076                        log.debug("tag: " + tag.toUserDebugString() + ", stack: " + stack);
077                }
078
079                // Check tag type
080                if (tag.isOpen())
081                {
082                        // Push onto stack
083                        stack.push(tag);
084                }
085                else if (tag.isClose())
086                {
087                        assertOpenTagFor(tag);
088                }
089                else if (tag.isOpenClose())
090                {
091                        // Tag closes itself
092                        tag.setOpenTag(tag);
093                }
094        }
095
096        /**
097         * Bind close tag with its open tag and pop it from the stack.
098         * 
099         * @param closeTag
100         * @throws ParseException
101         */
102        private void assertOpenTagFor(ComponentTag closeTag) throws ParseException
103        {
104                // Check that there is something on the stack
105                if (stack.size() > 0)
106                {
107                        // Pop the top tag off the stack
108                        ComponentTag top = stack.pop();
109
110                        // If the name of the current close tag does not match the
111                        // tag on the stack then we may have a mismatched close tag
112                        boolean mismatch = !hasEqualTagName(top, closeTag);
113
114                        if (mismatch)
115                        {
116                                top.setHasNoCloseTag(true);
117
118                                // Pop any simple tags off the top of the stack
119                                while (mismatch && !requiresCloseTag(top.getName()))
120                                {
121                                        top.setHasNoCloseTag(true);
122
123                                        // Pop simple tag
124                                        if (stack.isEmpty())
125                                        {
126                                                break;
127                                        }
128                                        top = stack.pop();
129
130                                        // Does new top of stack mismatch too?
131                                        mismatch = !hasEqualTagName(top, closeTag);
132                                }
133
134                                // If adjusting for simple tags did not fix the problem,
135                                // it must be a real mismatch.
136                                if (mismatch)
137                                {
138                                        throw new ParseException("Tag " + top.toUserDebugString() +
139                                                " has a mismatched close tag at " + closeTag.toUserDebugString(),
140                                                top.getPos());
141                                }
142                        }
143
144                        // Tag matches, so add pointer to matching tag
145                        closeTag.setOpenTag(top);
146                }
147                else
148                {
149                        throw new WicketParseException("Tag does not have a matching open tag:", closeTag);
150                }
151        }
152
153        private void validate() throws WicketParseException
154        {
155                ComponentTag top = getNotClosedTag();
156                if (top != null)
157                {
158                        throw new WicketParseException("Tag does not have a close tag:", top);
159                }
160        }
161
162        /**
163         * @return not closed tag
164         */
165        public ComponentTag getNotClosedTag()
166        {
167                // No more tags from the markup.
168                // If there's still a non-simple tag left, it's an error
169                if (stack.size() > 0)
170                {
171                        Iterator<ComponentTag> it = stack.descendingIterator();
172                        while (it.hasNext())
173                        {
174                                ComponentTag tag = it.next();
175                                if (!requiresCloseTag(tag.getName()))
176                                {
177                                        it.remove();
178                                }
179                                else
180                                {
181                                        return tag;
182                                }
183                        }
184                }
185                return null;
186        }
187
188        /**
189         * Configure this stack to call log.debug at operations
190         */
191        public void debug()
192        {
193                debug = true;
194        }
195
196        /**
197         * Gets whether this tag does not require a closing tag.
198         * 
199         * @param name
200         *            The tag's name, e.g. a, br, div, etc.
201         * @return True if this tag does not require a closing tag
202         */
203        public static boolean requiresCloseTag(final String name)
204        {
205                return doesNotRequireCloseTag.get(name.toLowerCase(Locale.ROOT)) == null;
206        }
207
208        /**
209         * Compare tag name including namespace
210         * 
211         * @param tag1
212         * @param tag2
213         * @return true if name and namespace are equal
214         */
215        public static boolean hasEqualTagName(final ComponentTag tag1, final ComponentTag tag2)
216        {
217                if (!tag1.getName().equalsIgnoreCase(tag2.getName()))
218                {
219                        return false;
220                }
221
222                if ((tag1.getNamespace() == null) && (tag2.getNamespace() == null))
223                {
224                        return true;
225                }
226
227                if ((tag1.getNamespace() != null) && (tag2.getNamespace() != null))
228                {
229                        return tag1.getNamespace().equalsIgnoreCase(tag2.getNamespace());
230                }
231
232                return false;
233        }
234}