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.HashMap;
022import java.util.Locale;
023import java.util.Map;
024
025import org.apache.wicket.markup.ComponentTag;
026import org.apache.wicket.markup.Markup;
027import org.apache.wicket.markup.MarkupElement;
028import org.apache.wicket.markup.MarkupException;
029import org.apache.wicket.markup.WicketParseException;
030import org.apache.wicket.markup.parser.AbstractMarkupFilter;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034
035/**
036 * This is a markup inline filter. It identifies HTML specific issues which make HTML not 100% xml
037 * compliant. E.g. tags like <p> often are missing the corresponding close tag.
038 * 
039 * @author Juergen Donnerstag
040 */
041public final class HtmlHandler extends AbstractMarkupFilter
042{
043        /** Logging */
044        private static final Logger log = LoggerFactory.getLogger(HtmlHandler.class);
045
046        /** Tag stack to find balancing tags */
047        final private ArrayDeque<ComponentTag> stack = new ArrayDeque<ComponentTag>();
048
049        /** Map of simple tags. */
050        private static final Map<String, Boolean> doesNotRequireCloseTag = new HashMap<String, Boolean>();
051
052        static
053        {
054                // Tags which are allowed not be closed in HTML
055                // http://www.w3.org/TR/html5/syntax.html#void-elements
056
057                // HTML5 doesn't allow void <p> but we should keep it for backward compatibility
058                doesNotRequireCloseTag.put("p", Boolean.TRUE);
059                doesNotRequireCloseTag.put("br", Boolean.TRUE);
060                doesNotRequireCloseTag.put("img", Boolean.TRUE);
061                doesNotRequireCloseTag.put("input", Boolean.TRUE);
062                doesNotRequireCloseTag.put("hr", Boolean.TRUE);
063                doesNotRequireCloseTag.put("link", Boolean.TRUE);
064                doesNotRequireCloseTag.put("meta", Boolean.TRUE);
065                doesNotRequireCloseTag.put("area", Boolean.TRUE);
066                doesNotRequireCloseTag.put("base", Boolean.TRUE);
067                doesNotRequireCloseTag.put("col", Boolean.TRUE);
068                doesNotRequireCloseTag.put("command", Boolean.TRUE);
069                doesNotRequireCloseTag.put("embed", Boolean.TRUE);
070                doesNotRequireCloseTag.put("keygen", Boolean.TRUE);
071                doesNotRequireCloseTag.put("param", Boolean.TRUE);
072                doesNotRequireCloseTag.put("source", Boolean.TRUE);
073                doesNotRequireCloseTag.put("track", Boolean.TRUE);
074                doesNotRequireCloseTag.put("wbr", Boolean.TRUE);
075        }
076
077        /**
078         * Construct.
079         */
080        public HtmlHandler()
081        {
082        }
083
084        @Override
085        public void postProcess(final Markup markup)
086        {
087                // If there's still a non-simple tag left, it's an error
088                while (stack.size() > 0)
089                {
090                        final ComponentTag top = stack.peek();
091
092                        if (!requiresCloseTag(top.getName()))
093                        {
094                                stack.pop();
095                                top.setHasNoCloseTag(true);
096                        }
097                        else
098                        {
099                                throw new MarkupException(markup, "Tag does not have a close tag", null);
100                        }
101                }
102        }
103
104        @Override
105        protected MarkupElement onComponentTag(final ComponentTag tag) throws ParseException
106        {
107                // Check tag type
108                if (tag.isOpen())
109                {
110                    // Check if open tags contains a "wicket:id" component
111                    setContainsWicketIdFlag(tag);
112                    
113                        // Push onto stack
114                        stack.push(tag);
115                }
116                else if (tag.isClose())
117                {
118                        // Check that there is something on the stack
119                        if (stack.size() > 0)
120                        {
121                                // Pop the top tag off the stack
122                                ComponentTag top = stack.pop();
123
124                                // If the name of the current close tag does not match the
125                                // tag on the stack then we may have a mismatched close tag
126                                boolean mismatch = !hasEqualTagName(top, tag);
127
128                                if (mismatch)
129                                {
130                                        // Pop any simple tags off the top of the stack
131                                        while (mismatch && !requiresCloseTag(top.getName()))
132                                        {
133                                                top.setHasNoCloseTag(true);
134                                                top.setContainsWicketId(false);
135                                                
136                                                // Pop simple tag
137                                                if (stack.isEmpty())
138                                                {
139                                                        break;
140                                                }
141                                                top = stack.pop();
142
143                                                // Does new top of stack mismatch too?
144                                                mismatch = !hasEqualTagName(top, tag);
145                                        }
146
147                                        // If adjusting for simple tags did not fix the problem,
148                                        // it must be a real mismatch.
149                                        if (mismatch)
150                                        {
151                                                throw new ParseException("Tag " + top.toUserDebugString() +
152                                                        " has a mismatched close tag at " + tag.toUserDebugString(),
153                                                        top.getPos());
154                                        }
155                                }
156
157                                // Tag matches, so add pointer to matching tag
158                                tag.setOpenTag(top);
159                        }
160                        else
161                        {
162                                throw new WicketParseException("Tag does not have a matching open tag:", tag);
163                        }
164                }
165                else if (tag.isOpenClose())
166                {
167                        // Tag closes itself
168                        tag.setOpenTag(tag);
169                }
170
171                return tag;
172        }
173        
174        /**
175         * Checks if the tag is a Wicket component explicitly added. i.e 
176         * it has the "wicket:id" attribute.
177         * 
178         * @param tag
179         */
180        private void setContainsWicketIdFlag(ComponentTag tag)
181        {
182                // check if it is a wicket:id component
183                String wicketIdAttr = getWicketNamespace() + ":" + "id";
184                boolean hasWicketId = tag.getAttributes().get(wicketIdAttr) != null;
185
186                if (hasWicketId)
187                {
188                        for (ComponentTag componentTag : stack)
189                        {
190                                componentTag.setContainsWicketId(hasWicketId);
191                        }
192                }
193        }
194
195    /**
196         * Gets whether this tag does not require a closing tag.
197         * 
198         * @param name
199         *            The tag's name, e.g. a, br, div, etc.
200         * @return True if this tag does not require a closing tag
201         */
202        public static boolean requiresCloseTag(final String name)
203        {
204                return doesNotRequireCloseTag.get(name.toLowerCase(Locale.ROOT)) == null;
205        }
206
207        /**
208         * Compare tag name including namespace
209         * 
210         * @param tag1
211         * @param tag2
212         * @return true if name and namespace are equal
213         */
214        public static boolean hasEqualTagName(final ComponentTag tag1, final ComponentTag tag2)
215        {
216                if (!tag1.getName().equalsIgnoreCase(tag2.getName()))
217                {
218                        return false;
219                }
220
221                if ((tag1.getNamespace() == null) && (tag2.getNamespace() == null))
222                {
223                        return true;
224                }
225
226                if ((tag1.getNamespace() != null) && (tag2.getNamespace() != null))
227                {
228                        return tag1.getNamespace().equalsIgnoreCase(tag2.getNamespace());
229                }
230
231                return false;
232        }
233}