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; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.time.Instant; 022import java.util.Locale; 023import java.util.regex.Matcher; 024import java.util.regex.Pattern; 025import org.apache.wicket.Component; 026import org.apache.wicket.core.util.lang.WicketObjects; 027import org.apache.wicket.util.lang.Args; 028import org.apache.wicket.util.lang.Bytes; 029import org.apache.wicket.util.resource.IFixedLocationResourceStream; 030import org.apache.wicket.util.resource.IResourceStream; 031import org.apache.wicket.util.resource.ResourceStreamNotFoundException; 032import org.apache.wicket.util.string.Strings; 033import org.slf4j.Logger; 034import org.slf4j.LoggerFactory; 035 036 037/** 038 * An IResourceStream implementation with specific extensions for markup resource streams. 039 * 040 * @author Juergen Donnerstag 041 */ 042public class MarkupResourceStream implements IResourceStream, IFixedLocationResourceStream 043{ 044 private static final long serialVersionUID = 1846489965076612828L; 045 046 private static final Logger log = LoggerFactory.getLogger(MarkupResourceStream.class); 047 048 /** */ 049 public static final String WICKET_XHTML_DTD = "http://wicket.apache.org/dtds.data/wicket-xhtml1.4-strict.dtd"; 050 051 private static final Pattern DOCTYPE_REGEX = Pattern.compile("!DOCTYPE\\s+(.*)\\s*"); 052 053 /** The associated markup resource stream */ 054 private final IResourceStream resourceStream; 055 056 /** Container info like Class, locale and style which were used to locate the resource */ 057 private final transient ContainerInfo containerInfo; 058 059 /** 060 * The actual component class the markup is directly associated with. It might be super class of 061 * the component class 062 */ 063 private final String markupClassName; 064 065 /** The key used to cache the markup resource stream */ 066 private String cacheKey; 067 068 /** In case of the inherited markup, this is the base markup */ 069 private transient Markup baseMarkup; 070 071 /** The encoding as found in <?xml ... encoding="" ?>. {@code null}, otherwise */ 072 private String encoding; 073 074 /** Wicket namespace: see WICKET_XHTML_DTD */ 075 private String wicketNamespace; 076 077 /** == wicket namespace name + ":id" */ 078 private String wicketId; 079 080 /** HTML5 http://www.w3.org/TR/html5-diff/#doctype */ 081 private String doctype; 082 083 /** 084 * Construct. 085 * 086 * @param resourceStream 087 */ 088 public MarkupResourceStream(final IResourceStream resourceStream) 089 { 090 this(resourceStream, null, null); 091 } 092 093 /** 094 * Construct. 095 * 096 * @param resourceStream 097 * @param containerInfo 098 * @param markupClass 099 */ 100 public MarkupResourceStream(final IResourceStream resourceStream, 101 final ContainerInfo containerInfo, final Class<?> markupClass) 102 { 103 this.resourceStream = Args.notNull(resourceStream, "resourceStream"); 104 this.containerInfo = containerInfo; 105 markupClassName = markupClass == null ? null : markupClass.getName(); 106 107 setWicketNamespace(MarkupParser.WICKET); 108 } 109 110 @Override 111 public String locationAsString() 112 { 113 if (resourceStream instanceof IFixedLocationResourceStream) 114 { 115 return ((IFixedLocationResourceStream)resourceStream).locationAsString(); 116 } 117 return null; 118 } 119 120 @Override 121 public void close() throws IOException 122 { 123 resourceStream.close(); 124 } 125 126 @Override 127 public String getContentType() 128 { 129 return resourceStream.getContentType(); 130 } 131 132 @Override 133 public InputStream getInputStream() throws ResourceStreamNotFoundException 134 { 135 return resourceStream.getInputStream(); 136 } 137 138 @Override 139 public Locale getLocale() 140 { 141 return resourceStream.getLocale(); 142 } 143 144 @Override 145 public Instant lastModifiedTime() 146 { 147 return resourceStream.lastModifiedTime(); 148 } 149 150 @Override 151 public Bytes length() 152 { 153 return resourceStream.length(); 154 } 155 156 @Override 157 public void setLocale(Locale locale) 158 { 159 resourceStream.setLocale(locale); 160 } 161 162 /** 163 * Get the actual component class the markup is directly associated with. Note: it not 164 * necessarily must be the container class. 165 * 166 * @return The directly associated class 167 */ 168 public Class<? extends Component> getMarkupClass() 169 { 170 if (markupClassName == null) 171 { 172 throw new MarkupException("no associated markup class"); 173 } 174 return WicketObjects.resolveClass(markupClassName); 175 } 176 177 /** 178 * Get the container info associated with the markup 179 * 180 * @return ContainerInfo 181 */ 182 public ContainerInfo getContainerInfo() 183 { 184 return containerInfo; 185 } 186 187 /** 188 * Gets cacheKey. 189 * 190 * @return cacheKey 191 */ 192 public final String getCacheKey() 193 { 194 return cacheKey; 195 } 196 197 /** 198 * Set the cache key 199 * 200 * @param cacheKey 201 */ 202 public final void setCacheKey(final String cacheKey) 203 { 204 this.cacheKey = cacheKey; 205 } 206 207 /** 208 * Gets the resource that contains this markup 209 * 210 * @return The resource where this markup came from 211 */ 212 public IResourceStream getResource() 213 { 214 return resourceStream; 215 } 216 217 /** 218 * Gets the markup encoding. A markup encoding may be specified in a markup file with an XML 219 * encoding specifier of the form <?xml ... encoding="..." ?>. 220 * 221 * @return Encoding, or null if not found. 222 */ 223 public String getEncoding() 224 { 225 return encoding; 226 } 227 228 /** 229 * Get the wicket namespace valid for this specific markup 230 * 231 * @return wicket namespace 232 */ 233 public String getWicketNamespace() 234 { 235 return wicketNamespace; 236 } 237 238 /** 239 * 240 * @return usually it is "wicket:id" 241 */ 242 final public String getWicketId() 243 { 244 return wicketId; 245 } 246 247 /** 248 * Sets encoding. 249 * 250 * @param encoding 251 * encoding 252 */ 253 final void setEncoding(final String encoding) 254 { 255 this.encoding = encoding; 256 } 257 258 /** 259 * Sets wicketNamespace. 260 * 261 * @param wicketNamespace 262 * wicketNamespace 263 */ 264 public final void setWicketNamespace(final String wicketNamespace) 265 { 266 this.wicketNamespace = wicketNamespace; 267 wicketId = (wicketNamespace + ":id").intern(); 268 269 if (!MarkupParser.WICKET.equals(wicketNamespace) && log.isDebugEnabled()) 270 { 271 log.debug("You are using a non-standard namespace name: '{}'", wicketNamespace); 272 } 273 } 274 275 /** 276 * Get the resource stream containing the base markup (markup inheritance) 277 * 278 * @return baseMarkupResource Null, if not base markup 279 */ 280 public MarkupResourceStream getBaseMarkupResourceStream() 281 { 282 if (baseMarkup == null) 283 { 284 return null; 285 } 286 return baseMarkup.getMarkupResourceStream(); 287 } 288 289 /** 290 * In case of markup inheritance, the base markup. 291 * 292 * @param baseMarkup 293 * The base markup 294 */ 295 public void setBaseMarkup(Markup baseMarkup) 296 { 297 this.baseMarkup = baseMarkup; 298 } 299 300 /** 301 * In case of markup inheritance, the base markup resource. 302 * 303 * @return The base markup 304 */ 305 public Markup getBaseMarkup() 306 { 307 return baseMarkup; 308 } 309 310 @Override 311 public String getStyle() 312 { 313 return resourceStream.getStyle(); 314 } 315 316 @Override 317 public String getVariation() 318 { 319 return resourceStream.getVariation(); 320 } 321 322 @Override 323 public void setStyle(String style) 324 { 325 resourceStream.setStyle(style); 326 } 327 328 @Override 329 public void setVariation(String variation) 330 { 331 resourceStream.setVariation(variation); 332 } 333 334 @Override 335 public String toString() 336 { 337 if (resourceStream != null) 338 { 339 return resourceStream.toString(); 340 } 341 else 342 { 343 return "(unknown resource)"; 344 } 345 } 346 347 /** 348 * Gets doctype. 349 * 350 * @return The doctype excluding 'DOCTYPE' 351 */ 352 public final String getDoctype() 353 { 354 if (doctype == null) 355 { 356 MarkupResourceStream baseMarkupResourceStream = getBaseMarkupResourceStream(); 357 if (baseMarkupResourceStream != null) 358 { 359 doctype = baseMarkupResourceStream.getDoctype(); 360 } 361 } 362 363 return doctype; 364 } 365 366 /** 367 * Sets doctype. 368 * 369 * @param doctype 370 * doctype 371 */ 372 public final void setDoctype(final CharSequence doctype) 373 { 374 if (Strings.isEmpty(doctype) == false) 375 { 376 String doc = doctype.toString().replaceAll("[\n\r]+", ""); 377 doc = doc.replaceAll("\\s+", " "); 378 Matcher matcher = DOCTYPE_REGEX.matcher(doc); 379 if (matcher.matches() == false) 380 { 381 throw new MarkupException("Invalid DOCTYPE: '" + doctype + "'"); 382 } 383 this.doctype = matcher.group(1).trim(); 384 } 385 } 386 387 /** 388 * @see <a href="http://www.w3.org/TR/html5-diff/#doctype">DOCTYPE</a> 389 * @return True, if doctype == <!DOCTYPE html> 390 */ 391 public boolean isHtml5() 392 { 393 return "html".equalsIgnoreCase(getDoctype()); 394 } 395}