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}