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}