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; 020 021import org.apache.wicket.Component; 022import org.apache.wicket.MarkupContainer; 023import org.apache.wicket.markup.ComponentTag; 024import org.apache.wicket.markup.ComponentTag.IAutoComponentFactory; 025import org.apache.wicket.markup.Markup; 026import org.apache.wicket.markup.MarkupElement; 027import org.apache.wicket.markup.MarkupException; 028import org.apache.wicket.markup.MarkupStream; 029import org.apache.wicket.markup.html.internal.HtmlHeaderContainer; 030import org.apache.wicket.markup.html.internal.HtmlHeaderItemsContainer; 031import org.apache.wicket.markup.parser.AbstractMarkupFilter; 032import org.apache.wicket.markup.parser.XmlTag.TagType; 033import org.apache.wicket.markup.resolver.HtmlHeaderResolver; 034import org.apache.wicket.util.tester.BaseWicketTester; 035 036 037/** 038 * This is a markup inline filter. 039 * <p> 040 * It assumes that {@link org.apache.wicket.markup.parser.filter.WicketTagIdentifier} 041 * has been called first and search for a <head> tag (note: not wicket:head). Provided the markup contains a 042 * <body> tag it will automatically prepend a <head> tag if missing. 043 * </p> 044 * <p> 045 * Additionally this filter handles <wicket:header-items/>. If there is such tag then it is marked 046 * as the one that should be used as {@link org.apache.wicket.markup.html.internal.HtmlHeaderContainer}, by 047 * setting its id to {@value #HEADER_ID}. 048 * </p> 049 * <p> 050 * Note: This handler is only relevant for Pages (see MarkupParser.newFilterChain()) 051 * 052 * @see org.apache.wicket.markup.MarkupParser 053 * @see org.apache.wicket.markup.resolver.HtmlHeaderResolver 054 * @author Juergen Donnerstag 055 */ 056public final class HtmlHeaderSectionHandler extends AbstractMarkupFilter 057{ 058 public static final String BODY = "body"; 059 public static final String HEAD = "head"; 060 061 /** The automatically assigned wicket:id to >head< tag */ 062 public static final String HEADER_ID = "_header_"; 063 064 public static final String HEADER_ID_ITEM = "_header_item_"; 065 066 /** True if <head> has been found already */ 067 private boolean foundHead = false; 068 069 /** True if </head> has been found already */ 070 private boolean foundClosingHead = false; 071 072 /** True if </wicket:header-items> has been found already */ 073 private boolean foundHeaderItemsTag = false; 074 075 /** True if all the rest of the markup file can be ignored */ 076 private boolean ignoreTheRest = false; 077 078 /** The Markup available so far for the resource */ 079 private final Markup markup; 080 081 private static final IAutoComponentFactory HTML_HEADER_FACTORY = new IAutoComponentFactory() 082 { 083 @Override 084 public Component newComponent(MarkupContainer container, ComponentTag tag) 085 { 086 return new HtmlHeaderContainer(tag.getId()); 087 } 088 }; 089 090 private static final IAutoComponentFactory HTML_HEADER_ITEMS_FACTORY = new IAutoComponentFactory() 091 { 092 @Override 093 public Component newComponent(MarkupContainer container, ComponentTag tag) 094 { 095 return new HtmlHeaderItemsContainer(tag.getId()); 096 } 097 }; 098 099 /** 100 * Construct. 101 * 102 * @param markup 103 * The Markup object being filled while reading the markup resource 104 */ 105 public HtmlHeaderSectionHandler(final Markup markup) 106 { 107 super(markup.getMarkupResourceStream()); 108 this.markup = markup; 109 } 110 111 @Override 112 protected final MarkupElement onComponentTag(ComponentTag tag) throws ParseException 113 { 114 // Whatever there is left in the markup, ignore it 115 if (ignoreTheRest == true) 116 { 117 return tag; 118 } 119 120 // if it is <head> or </head> 121 if (HEAD.equalsIgnoreCase(tag.getName())) 122 { 123 if (tag.getNamespace() == null) 124 { 125 handleHeadTag(tag); 126 } 127 else 128 { 129 // we found <wicket:head> 130 foundHead = true; 131 foundClosingHead = true; 132 } 133 } 134 else if (HtmlHeaderResolver.HEADER_ITEMS.equalsIgnoreCase(tag.getName()) && 135 tag.getNamespace().equalsIgnoreCase(getWicketNamespace())) 136 { 137 handleHeaderItemsTag(tag); 138 } 139 else if (BODY.equalsIgnoreCase(tag.getName()) && (tag.getNamespace() == null)) 140 { 141 handleBodyTag(); 142 } 143 144 return tag; 145 } 146 147 /** 148 * Handle tag <body> 149 */ 150 private void handleBodyTag() 151 { 152 // WICKET-4511: We found <body> inside <head> tag. Markup is not valid! 153 if (foundHead && !foundClosingHead) 154 { 155 throw new MarkupException(new MarkupStream(markup), 156 "Invalid page markup. Tag <BODY> found inside <HEAD>"); 157 } 158 159 // We found <body> 160 if (foundHead == false) 161 { 162 insertHeadTag(); 163 } 164 165 // <head> must always be before <body> 166 ignoreTheRest = true; 167 } 168 169 /** 170 * Handle tag <wicket:header-items> 171 * 172 * @param tag 173 */ 174 private void handleHeaderItemsTag(ComponentTag tag) 175 { 176 if ((tag.isOpen() || tag.isOpenClose()) && foundHeaderItemsTag) 177 { 178 throw new MarkupException(new MarkupStream(markup), 179 "More than one <wicket:header-items/> detected in the <head> element. Only one is allowed."); 180 } 181 else if (foundClosingHead) 182 { 183 throw new MarkupException(new MarkupStream(markup), 184 "Detected <wicket:header-items/> after the closing </head> element."); 185 } 186 187 foundHeaderItemsTag = true; 188 tag.setId(HEADER_ID); 189 tag.setAutoComponentTag(true); 190 tag.setModified(true); 191 tag.setAutoComponentFactory(HTML_HEADER_ITEMS_FACTORY); 192 } 193 194 /** 195 * Handle tag <head> 196 * @param tag 197 */ 198 private void handleHeadTag(ComponentTag tag) 199 { 200 // we found <head> 201 if (tag.isOpen()) 202 { 203 if(foundHead) 204 { 205 throw new MarkupException(new MarkupStream(markup), 206 "Tag <head> is not allowed at this position (do you have multiple <head> tags in your markup?)."); 207 } 208 209 foundHead = true; 210 211 if (tag.getId() == null) 212 { 213 tag.setId(HEADER_ID); 214 tag.setAutoComponentTag(true); 215 tag.setModified(true); 216 tag.setAutoComponentFactory(HTML_HEADER_FACTORY); 217 } 218 } 219 else if (tag.isClose()) 220 { 221 if (foundHeaderItemsTag) 222 { 223 // revert the settings from above 224 ComponentTag headOpenTag = tag.getOpenTag(); 225 // change the id because it is special. See HtmlHeaderResolver 226 headOpenTag.setId(HEADER_ID + "-Ignored"); 227 headOpenTag.setAutoComponentTag(false); 228 headOpenTag.setModified(false); 229 headOpenTag.setFlag(ComponentTag.RENDER_RAW, true); 230 headOpenTag.setAutoComponentFactory(null); 231 } 232 233 foundClosingHead = true; 234 } 235 } 236 237 /** 238 * Insert <head> open and close tag (with empty body) to the current position. 239 */ 240 private void insertHeadTag() 241 { 242 // Note: only the open-tag must be a AutoComponentTag 243 final ComponentTag openTag = new ComponentTag(HEAD, TagType.OPEN); 244 openTag.setId(HEADER_ID); 245 openTag.setAutoComponentTag(true); 246 openTag.setModified(true); 247 openTag.setAutoComponentFactory(HTML_HEADER_FACTORY); 248 249 final ComponentTag closeTag = new ComponentTag(HEAD, TagType.CLOSE); 250 closeTag.setOpenTag(openTag); 251 closeTag.setModified(true); 252 253 // insert the tags into the markup stream 254 markup.addMarkupElement(openTag); 255 markup.addMarkupElement(closeTag); 256 } 257}