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.request.resource; 018 019import java.io.ByteArrayInputStream; 020import java.io.IOException; 021import java.io.InputStream; 022import java.io.Serializable; 023import java.nio.charset.Charset; 024import java.nio.charset.StandardCharsets; 025import java.time.Instant; 026import java.util.Locale; 027import java.util.Objects; 028import javax.servlet.http.HttpServletResponse; 029import org.apache.wicket.Application; 030import org.apache.wicket.IWicketInternalException; 031import org.apache.wicket.Session; 032import org.apache.wicket.WicketRuntimeException; 033import org.apache.wicket.core.util.lang.WicketObjects; 034import org.apache.wicket.core.util.resource.locator.IResourceStreamLocator; 035import org.apache.wicket.javascript.IJavaScriptCompressor; 036import org.apache.wicket.markup.html.IPackageResourceGuard; 037import org.apache.wicket.mock.MockWebRequest; 038import org.apache.wicket.request.Url; 039import org.apache.wicket.request.cycle.RequestCycle; 040import org.apache.wicket.request.resource.caching.IStaticCacheableResource; 041import org.apache.wicket.resource.IScopeAwareTextResourceProcessor; 042import org.apache.wicket.resource.ITextResourceCompressor; 043import org.apache.wicket.response.StringResponse; 044import org.apache.wicket.util.io.IOUtils; 045import org.apache.wicket.util.lang.Classes; 046import org.apache.wicket.util.lang.Packages; 047import org.apache.wicket.util.resource.IFixedLocationResourceStream; 048import org.apache.wicket.util.resource.IResourceStream; 049import org.apache.wicket.util.resource.ResourceStreamNotFoundException; 050import org.apache.wicket.util.resource.ResourceStreamWrapper; 051import org.apache.wicket.util.string.Strings; 052import org.slf4j.Logger; 053import org.slf4j.LoggerFactory; 054 055/** 056 * Represents a localizable static resource. 057 * <p> 058 * Use like eg: 059 * 060 * <pre> 061 * MyPackageResource IMG_UNKNOWN = new MyPackageResource(EditPage.class, "questionmark.gif"); 062 * </pre> 063 * 064 * where the static resource references image 'questionmark.gif' from the the package that EditPage 065 * is in to get a package resource. 066 * </p> 067 * 068 * Access to resources can be granted or denied via a {@link IPackageResourceGuard}. Please see 069 * {@link org.apache.wicket.settings.ResourceSettings#getPackageResourceGuard()} as well. 070 * 071 * @author Jonathan Locke 072 * @author Eelco Hillenius 073 * @author Juergen Donnerstag 074 * @author Matej Knopp 075 * @author Tobias Soloschenko 076 */ 077public class PackageResource extends AbstractResource implements IStaticCacheableResource 078{ 079 private static final Logger log = LoggerFactory.getLogger(PackageResource.class); 080 081 private static final long serialVersionUID = 1L; 082 083 /** 084 * Exception thrown when the creation of a package resource is not allowed. 085 */ 086 public static final class PackageResourceBlockedException extends WicketRuntimeException 087 implements 088 IWicketInternalException 089 { 090 private static final long serialVersionUID = 1L; 091 092 /** 093 * Construct. 094 * 095 * @param message 096 * error message 097 */ 098 public PackageResourceBlockedException(String message) 099 { 100 super(message); 101 } 102 } 103 104 /** 105 * The path to the resource 106 */ 107 private final String absolutePath; 108 109 /** 110 * The resource's locale 111 */ 112 private final Locale locale; 113 114 /** 115 * The path this resource was created with. 116 */ 117 private final String path; 118 119 /** 120 * The scoping class, used for class loading and to determine the package. 121 */ 122 private final String scopeName; 123 124 /** 125 * The name of the resource 126 */ 127 private final String name; 128 129 /** 130 * The resource's style 131 */ 132 private final String style; 133 134 /** 135 * The component's variation (of the style) 136 */ 137 private final String variation; 138 139 /** 140 * A flag indicating whether {@code ITextResourceCompressor} can be used to compress this 141 * resource. Default is {@code false} because this resource may be used for binary data (e.g. an 142 * image). Specializations of this class should change this flag appropriately. 143 */ 144 private boolean compress = false; 145 146 /** 147 * controls whether {@link org.apache.wicket.request.resource.caching.IResourceCachingStrategy} 148 * should be applied to resource 149 */ 150 private boolean cachingEnabled = true; 151 152 /** 153 * text encoding (may be null) - only makes sense for character-based resources 154 */ 155 private String textEncoding = null; 156 157 /** 158 * Reads the resource buffered - the content is copied into memory 159 */ 160 private boolean readBuffered = true; 161 162 /** 163 * Hidden constructor. 164 * 165 * @param scope 166 * This argument will be used to get the class loader for loading the package 167 * resource, and to determine what package it is in 168 * @param name 169 * The relative path to the resource 170 * @param locale 171 * The locale of the resource 172 * @param style 173 * The style of the resource 174 * @param variation 175 * The component's variation (of the style) 176 */ 177 protected PackageResource(final Class<?> scope, final String name, final Locale locale, 178 final String style, final String variation) 179 { 180 // Convert resource path to absolute path relative to base package 181 absolutePath = Packages.absolutePath(scope, name); 182 183 final String parentEscape = getParentFolderPlaceholder(); 184 185 if (Strings.isEmpty(parentEscape) == false) 186 { 187 path = Strings.replaceAll(name, "../", parentEscape + "/").toString(); 188 } 189 else 190 { 191 path = name; 192 } 193 194 this.name = name; 195 this.scopeName = scope.getName(); 196 this.locale = locale; 197 this.style = style; 198 this.variation = variation; 199 } 200 201 private Locale getCurrentLocale() 202 { 203 if (locale == null && Session.exists()) 204 { 205 return Session.get().getLocale(); 206 } 207 208 return locale; 209 } 210 211 private String getCurrentStyle() 212 { 213 if (style == null && Session.exists()) 214 { 215 return Session.get().getStyle(); 216 } 217 218 return style; 219 } 220 221 /** 222 * Returns true if the caching for this resource is enabled 223 * 224 * @return if the caching is enabled 225 */ 226 @Override 227 public boolean isCachingEnabled() 228 { 229 return cachingEnabled; 230 } 231 232 /** 233 * Sets the caching for this resource to be enabled 234 * 235 * @param enabled 236 * if the cacheing should be enabled 237 */ 238 public void setCachingEnabled(final boolean enabled) 239 { 240 this.cachingEnabled = enabled; 241 } 242 243 /** 244 * get text encoding (intented for character-based resources) 245 * 246 * @return custom encoding or {@code null} to use default 247 */ 248 public String getTextEncoding() 249 { 250 return textEncoding; 251 } 252 253 /** 254 * set text encoding (intented for character-based resources) 255 * 256 * @param textEncoding 257 * custom encoding or {@code null} to use default 258 */ 259 public void setTextEncoding(final String textEncoding) 260 { 261 this.textEncoding = textEncoding; 262 } 263 264 @Override 265 public Serializable getCacheKey() 266 { 267 Class<?> scope = getScope(); 268 String currentStyle = getCurrentStyle(); 269 Locale currentLocale = getCurrentLocale(); 270 271 IResourceStream packageResource = Application.get() 272 .getResourceSettings() 273 .getResourceStreamLocator() 274 .locate(scope, absolutePath, currentStyle, variation, currentLocale, null, false); 275 276 // if resource stream can not be found do not cache 277 if (packageResource != null) 278 { 279 return new CacheKey(scopeName, absolutePath, currentLocale, currentStyle, variation); 280 } 281 282 return null; 283 } 284 285 /** 286 * Gets the scoping class, used for class loading and to determine the package. 287 * 288 * @return the scoping class 289 */ 290 public final Class<?> getScope() 291 { 292 return WicketObjects.resolveClass(scopeName); 293 } 294 295 public final String getName() 296 { 297 return name; 298 } 299 300 /** 301 * Gets the style. 302 * 303 * @return the style 304 */ 305 public final String getStyle() 306 { 307 return style; 308 } 309 310 /** 311 * creates a new resource response based on the request attributes 312 * 313 * @param attributes 314 * current request attributes from client 315 * @return resource response for answering request 316 */ 317 @Override 318 protected ResourceResponse newResourceResponse(Attributes attributes) 319 { 320 final ResourceResponse resourceResponse = new ResourceResponse(); 321 322 final IResourceStream resourceStream = getResourceStream(); 323 324 // bail out if resource stream could not be found 325 if (resourceStream == null) 326 { 327 return sendResourceError(resourceResponse, HttpServletResponse.SC_NOT_FOUND, 328 "Unable to find resource"); 329 } 330 331 // add Last-Modified header (to support HEAD requests and If-Modified-Since) 332 final Instant lastModified = resourceStream.lastModifiedTime(); 333 334 resourceResponse.setLastModified(lastModified); 335 336 if (resourceResponse.dataNeedsToBeWritten(attributes)) 337 { 338 String contentType = resourceStream.getContentType(); 339 340 if (contentType == null && Application.exists()) 341 { 342 contentType = Application.get().getMimeType(path); 343 } 344 345 // set Content-Type (may be null) 346 resourceResponse.setContentType(contentType); 347 348 // set content encoding (may be null) 349 resourceResponse.setTextEncoding(getTextEncoding()); 350 351 // supports accept range 352 resourceResponse.setAcceptRange(ContentRangeType.BYTES); 353 354 try 355 { 356 // read resource data to get the content length 357 InputStream inputStream = resourceStream.getInputStream(); 358 359 byte[] bytes = null; 360 // send Content-Length header 361 if (readBuffered) 362 { 363 bytes = IOUtils.toByteArray(inputStream); 364 resourceResponse.setContentLength(bytes.length); 365 } 366 else 367 { 368 resourceResponse.setContentLength(resourceStream.length().bytes()); 369 } 370 371 // get content range information 372 RequestCycle cycle = RequestCycle.get(); 373 Long startbyte = cycle.getMetaData(CONTENT_RANGE_STARTBYTE); 374 Long endbyte = cycle.getMetaData(CONTENT_RANGE_ENDBYTE); 375 376 // send response body with resource data 377 PartWriterCallback partWriterCallback = new PartWriterCallback(bytes != null 378 ? new ByteArrayInputStream(bytes) : inputStream, 379 resourceResponse.getContentLength(), startbyte, endbyte); 380 381 // If read buffered is set to false ensure the part writer callback is going to 382 // close the input stream 383 resourceResponse.setWriteCallback(partWriterCallback.setClose(!readBuffered)); 384 } 385 catch (IOException e) 386 { 387 log.debug(e.getMessage(), e); 388 return sendResourceError(resourceResponse, 500, "Unable to read resource stream"); 389 } 390 catch (ResourceStreamNotFoundException e) 391 { 392 log.debug(e.getMessage(), e); 393 return sendResourceError(resourceResponse, 500, "Unable to open resource stream"); 394 } 395 finally 396 { 397 try 398 { 399 if (readBuffered) 400 { 401 IOUtils.close(resourceStream); 402 } 403 } 404 catch (IOException e) 405 { 406 log.warn("Unable to close the resource stream", e); 407 } 408 } 409 } 410 411 return resourceResponse; 412 } 413 414 /** 415 * Gives a chance to modify the resource going to be written in the response 416 * 417 * @param attributes 418 * current request attributes from client 419 * @param original 420 * the original response 421 * @return the processed response 422 */ 423 protected byte[] processResponse(final Attributes attributes, final byte[] original) 424 { 425 return compressResponse(attributes, original); 426 } 427 428 /** 429 * Compresses the response if its is eligible and there is a configured compressor 430 * 431 * @param attributes 432 * * current request attributes from client 433 * * @param original 434 * * the original response 435 * * @return the compressed response 436 */ 437 protected byte[] compressResponse(final Attributes attributes, final byte[] original) 438 { 439 ITextResourceCompressor compressor = getCompressor(); 440 441 if (compressor != null && getCompress()) 442 { 443 try 444 { 445 Charset charset = getProcessingEncoding(); 446 String nonCompressed = new String(original, charset); 447 String output; 448 if (compressor instanceof IScopeAwareTextResourceProcessor) 449 { 450 IScopeAwareTextResourceProcessor scopeAwareProcessor = (IScopeAwareTextResourceProcessor)compressor; 451 output = scopeAwareProcessor.process(nonCompressed, getScope(), name); 452 } 453 else 454 { 455 output = compressor.compress(nonCompressed); 456 } 457 final String textEncoding = getTextEncoding(); 458 final Charset outputCharset; 459 if (Strings.isEmpty(textEncoding)) 460 { 461 outputCharset = charset; 462 } 463 else 464 { 465 outputCharset = Charset.forName(textEncoding); 466 } 467 return output.getBytes(outputCharset); 468 } 469 catch (Exception e) 470 { 471 log.error("Error while compressing the content", e); 472 return original; 473 } 474 } 475 else 476 { 477 // don't strip the comments 478 return original; 479 } 480 } 481 482 /** 483 * @return The charset to use to read the resource 484 */ 485 protected Charset getProcessingEncoding() 486 { 487 return StandardCharsets.UTF_8; 488 } 489 490 /** 491 * Gets the {@link IJavaScriptCompressor} to be used. By default returns the configured 492 * compressor on application level, but can be overriden by the user application to provide 493 * compressor specific to the resource. 494 * 495 * @return the configured application level JavaScript compressor. May be {@code null}. 496 */ 497 protected ITextResourceCompressor getCompressor() 498 { 499 return null; 500 } 501 502 /** 503 * send resource specific error message and write log entry 504 * 505 * @param resourceResponse 506 * resource response 507 * @param errorCode 508 * error code (=http status) 509 * @param errorMessage 510 * error message (=http error message) 511 * @return resource response for method chaining 512 */ 513 private ResourceResponse sendResourceError(ResourceResponse resourceResponse, int errorCode, 514 String errorMessage) 515 { 516 String msg = String.format( 517 "resource [path = %s, style = %s, variation = %s, locale = %s]: %s (status=%d)", 518 absolutePath, style, variation, locale, errorMessage, errorCode); 519 520 log.warn(msg); 521 522 resourceResponse.setError(errorCode, errorMessage); 523 return resourceResponse; 524 } 525 526 /** 527 * locate resource stream for current resource 528 * 529 * @return resource stream or <code>null</code> if not found 530 */ 531 @Override 532 public IResourceStream getResourceStream() 533 { 534 return internalGetResourceStream(getCurrentStyle(), getCurrentLocale()); 535 } 536 537 /** 538 * @return whether {@link org.apache.wicket.resource.ITextResourceCompressor} can be used to 539 * compress the resource. 540 */ 541 public boolean getCompress() 542 { 543 return compress; 544 } 545 546 /** 547 * @param compress 548 * A flag indicating whether the resource should be compressed. 549 */ 550 public void setCompress(boolean compress) 551 { 552 this.compress = compress; 553 } 554 555 private IResourceStream internalGetResourceStream(final String style, final Locale locale) 556 { 557 IResourceStreamLocator resourceStreamLocator = Application.get() 558 .getResourceSettings() 559 .getResourceStreamLocator(); 560 IResourceStream resourceStream = resourceStreamLocator.locate(getScope(), absolutePath, 561 style, variation, locale, null, false); 562 563 String realPath = absolutePath; 564 if (resourceStream instanceof IFixedLocationResourceStream) 565 { 566 realPath = ((IFixedLocationResourceStream)resourceStream).locationAsString(); 567 if (realPath != null) 568 { 569 int index = realPath.indexOf(absolutePath); 570 if (index != -1) 571 { 572 realPath = realPath.substring(index); 573 } 574 } 575 else 576 { 577 realPath = absolutePath; 578 } 579 580 } 581 582 if (accept(realPath) == false) 583 { 584 throw new PackageResourceBlockedException( 585 "Access denied to (static) package resource " + absolutePath + 586 ". See IPackageResourceGuard"); 587 } 588 589 if (resourceStream != null) 590 { 591 resourceStream = new ProcessingResourceStream(resourceStream); 592 } 593 return resourceStream; 594 } 595 596 /** 597 * An IResourceStream that processes the input stream of the original IResourceStream 598 */ 599 private class ProcessingResourceStream extends ResourceStreamWrapper 600 { 601 private static final long serialVersionUID = 1L; 602 603 private ProcessingResourceStream(IResourceStream delegate) 604 { 605 super(delegate); 606 } 607 608 @Override 609 public InputStream getInputStream() throws ResourceStreamNotFoundException 610 { 611 byte[] bytes = null; 612 InputStream inputStream = super.getInputStream(); 613 614 if (readBuffered) 615 { 616 try 617 { 618 bytes = IOUtils.toByteArray(inputStream); 619 } 620 catch (IOException iox) 621 { 622 throw new WicketRuntimeException(iox); 623 } 624 finally 625 { 626 IOUtils.closeQuietly(this); 627 } 628 } 629 630 RequestCycle cycle = RequestCycle.get(); 631 Attributes attributes; 632 if (cycle != null) 633 { 634 attributes = new Attributes(cycle.getRequest(), cycle.getResponse()); 635 } 636 else 637 { 638 // use empty request and response in case of non-http thread. WICKET-5532 639 attributes = new Attributes(new MockWebRequest(Url.parse("")), new StringResponse()); 640 } 641 if (bytes != null) 642 { 643 byte[] processedBytes = processResponse(attributes, bytes); 644 return new ByteArrayInputStream(processedBytes); 645 } 646 else 647 { 648 return inputStream; 649 } 650 } 651 } 652 653 /** 654 * Checks whether access is granted for this resource. 655 * 656 * By default IPackageResourceGuard is used to check the permissions but the resource itself can 657 * also make the check. 658 * 659 * @param path 660 * resource path 661 * @return <code>true<code> if resource access is granted 662 */ 663 protected boolean accept(String path) 664 { 665 IPackageResourceGuard guard = Application.get() 666 .getResourceSettings() 667 .getPackageResourceGuard(); 668 669 return guard.accept(path); 670 } 671 672 /** 673 * Checks whether a resource for a given set of criteria exists. 674 * 675 * @param key 676 * The key that contains all attributes about the requested resource 677 * @return {@code true} if there is a package resource with the given attributes 678 */ 679 public static boolean exists(final ResourceReference.Key key) 680 { 681 return exists(key.getScopeClass(), key.getName(), key.getLocale(), key.getStyle(), 682 key.getVariation()); 683 } 684 685 /** 686 * Checks whether a resource for a given set of criteria exists. 687 * 688 * @param scope 689 * This argument will be used to get the class loader for loading the package 690 * resource, and to determine what package it is in. Typically this is the class in 691 * which you call this method 692 * @param path 693 * The path to the resource 694 * @param locale 695 * The locale of the resource 696 * @param style 697 * The style of the resource (see {@link org.apache.wicket.Session}) 698 * @param variation 699 * The component's variation (of the style) 700 * @return {@code true} if a resource could be loaded, {@code false} otherwise 701 */ 702 public static boolean exists(final Class<?> scope, final String path, final Locale locale, 703 final String style, final String variation) 704 { 705 String absolutePath = Packages.absolutePath(scope, path); 706 return Application.get() 707 .getResourceSettings() 708 .getResourceStreamLocator() 709 .locate(scope, absolutePath, style, variation, locale, null, false) != null; 710 } 711 712 @Override 713 public String toString() 714 { 715 final StringBuilder result = new StringBuilder(); 716 result.append('[') 717 .append(Classes.simpleName(getClass())) 718 .append(' ') 719 .append("name = ") 720 .append(path) 721 .append(", scope = ") 722 .append(scopeName) 723 .append(", locale = ") 724 .append(locale) 725 .append(", style = ") 726 .append(style) 727 .append(", variation = ") 728 .append(variation) 729 .append(']'); 730 return result.toString(); 731 } 732 733 @Override 734 public int hashCode() 735 { 736 final int prime = 31; 737 int result = 1; 738 result = prime * result + ((absolutePath == null) ? 0 : absolutePath.hashCode()); 739 result = prime * result + ((locale == null) ? 0 : locale.hashCode()); 740 result = prime * result + ((path == null) ? 0 : path.hashCode()); 741 result = prime * result + ((scopeName == null) ? 0 : scopeName.hashCode()); 742 result = prime * result + ((style == null) ? 0 : style.hashCode()); 743 result = prime * result + ((variation == null) ? 0 : variation.hashCode()); 744 return result; 745 } 746 747 @Override 748 public boolean equals(Object obj) 749 { 750 if (this == obj) 751 return true; 752 if (obj == null) 753 return false; 754 if (getClass() != obj.getClass()) 755 return false; 756 757 PackageResource other = (PackageResource)obj; 758 759 return Objects.equals(absolutePath, other.absolutePath) && 760 Objects.equals(locale, other.locale) && Objects.equals(path, other.path) && 761 Objects.equals(scopeName, other.scopeName) && Objects.equals(style, other.style) && 762 Objects.equals(variation, other.variation); 763 } 764 765 String getParentFolderPlaceholder() 766 { 767 String parentFolderPlaceholder; 768 if (Application.exists()) 769 { 770 parentFolderPlaceholder = Application.get() 771 .getResourceSettings() 772 .getParentFolderPlaceholder(); 773 } 774 else 775 { 776 parentFolderPlaceholder = ".."; 777 } 778 return parentFolderPlaceholder; 779 } 780 781 private static class CacheKey implements Serializable 782 { 783 private final String scopeName; 784 private final String path; 785 private final Locale locale; 786 private final String style; 787 private final String variation; 788 789 public CacheKey(String scopeName, String path, Locale locale, String style, String variation) 790 { 791 this.scopeName = scopeName; 792 this.path = path; 793 this.locale = locale; 794 this.style = style; 795 this.variation = variation; 796 } 797 798 @Override 799 public boolean equals(Object o) 800 { 801 if (this == o) 802 return true; 803 if (!(o instanceof CacheKey)) 804 return false; 805 806 CacheKey cacheKey = (CacheKey)o; 807 808 return Objects.equals(locale, cacheKey.locale) && Objects.equals(path, cacheKey.path) && 809 Objects.equals(scopeName, cacheKey.scopeName) && 810 Objects.equals(style, cacheKey.style) && 811 Objects.equals(variation, cacheKey.variation); 812 } 813 814 @Override 815 public int hashCode() 816 { 817 int result = scopeName.hashCode(); 818 result = 31 * result + path.hashCode(); 819 result = 31 * result + (locale != null ? locale.hashCode() : 0); 820 result = 31 * result + (style != null ? style.hashCode() : 0); 821 result = 31 * result + (variation != null ? variation.hashCode() : 0); 822 return result; 823 } 824 825 @Override 826 public String toString() 827 { 828 final StringBuilder sb = new StringBuilder(); 829 sb.append("CacheKey"); 830 sb.append("{scopeName='").append(scopeName).append('\''); 831 sb.append(", path='").append(path).append('\''); 832 sb.append(", locale=").append(locale); 833 sb.append(", style='").append(style).append('\''); 834 sb.append(", variation='").append(variation).append('\''); 835 sb.append('}'); 836 return sb.toString(); 837 } 838 } 839 840 /** 841 * If the package resource should be read buffered.<br> 842 * <br> 843 * WARNING - if the stream is not read buffered compressors will not work, because they require 844 * the whole content to be read into memory.<br> 845 * ({@link org.apache.wicket.javascript.IJavaScriptCompressor}, <br> 846 * {@link org.apache.wicket.css.ICssCompressor}, <br> 847 * {@link org.apache.wicket.resource.IScopeAwareTextResourceProcessor}) 848 * 849 * @param readBuffered 850 * if the package resource should be read buffered 851 * @return the current package resource 852 */ 853 public PackageResource readBuffered(boolean readBuffered) 854 { 855 this.readBuffered = readBuffered; 856 return this; 857 } 858}