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.IOException; 020import java.io.InputStream; 021import java.time.Duration; 022import java.time.Instant; 023import java.time.temporal.ChronoUnit; 024import java.util.HashSet; 025import java.util.Locale; 026import java.util.Set; 027 028import jakarta.servlet.http.HttpServletResponse; 029 030import org.apache.wicket.Application; 031import org.apache.wicket.MetaDataKey; 032import org.apache.wicket.WicketRuntimeException; 033import org.apache.wicket.request.HttpHeaderCollection; 034import org.apache.wicket.request.Request; 035import org.apache.wicket.request.Response; 036import org.apache.wicket.request.cycle.RequestCycle; 037import org.apache.wicket.request.http.WebRequest; 038import org.apache.wicket.request.http.WebResponse; 039import org.apache.wicket.request.resource.caching.IResourceCachingStrategy; 040import org.apache.wicket.request.resource.caching.IStaticCacheableResource; 041import org.apache.wicket.util.io.Streams; 042import org.apache.wicket.util.lang.Args; 043import org.apache.wicket.util.lang.Classes; 044import org.apache.wicket.util.string.Strings; 045 046/** 047 * Convenience resource implementation. The subclass must implement 048 * {@link #newResourceResponse(org.apache.wicket.request.resource.IResource.Attributes)} method. 049 * 050 * @author Matej Knopp 051 * @author Tobias Soloschenko 052 */ 053public abstract class AbstractResource implements IResource 054{ 055 private static final long serialVersionUID = 1L; 056 057 /** header values that are managed internally and must not be set directly */ 058 public static final Set<String> INTERNAL_HEADERS; 059 060 /** The meta data key of the content range start byte **/ 061 public static final MetaDataKey<Long> CONTENT_RANGE_STARTBYTE = new MetaDataKey<>() 062 { 063 private static final long serialVersionUID = 1L; 064 }; 065 066 /** The meta data key of the content range end byte **/ 067 public static final MetaDataKey<Long> CONTENT_RANGE_ENDBYTE = new MetaDataKey<>() 068 { 069 private static final long serialVersionUID = 1L; 070 }; 071 072 public static final String CONTENT_DISPOSITION_HEADER_NAME = "content-disposition"; 073 074 /** 075 * All available content range types. The type name represents the name used in header 076 * information. 077 */ 078 public enum ContentRangeType 079 { 080 BYTES("bytes"), NONE("none"); 081 082 private final String typeName; 083 084 ContentRangeType(String typeName) 085 { 086 this.typeName = typeName; 087 } 088 089 public String getTypeName() 090 { 091 return typeName; 092 } 093 } 094 095 static 096 { 097 INTERNAL_HEADERS = new HashSet<>(); 098 INTERNAL_HEADERS.add("server"); 099 INTERNAL_HEADERS.add("date"); 100 INTERNAL_HEADERS.add("expires"); 101 INTERNAL_HEADERS.add("last-modified"); 102 INTERNAL_HEADERS.add("content-type"); 103 INTERNAL_HEADERS.add("content-length"); 104 INTERNAL_HEADERS.add(CONTENT_DISPOSITION_HEADER_NAME); 105 INTERNAL_HEADERS.add("transfer-encoding"); 106 INTERNAL_HEADERS.add("connection"); 107 INTERNAL_HEADERS.add("content-range"); 108 INTERNAL_HEADERS.add("accept-range"); 109 } 110 111 /** 112 * Construct. 113 */ 114 public AbstractResource() 115 { 116 } 117 118 /** 119 * Override this method to return a {@link ResourceResponse} for the request. 120 * 121 * @param attributes 122 * request attributes 123 * @return resource data instance 124 */ 125 protected abstract ResourceResponse newResourceResponse(Attributes attributes); 126 127 /** 128 * Represents data used to configure response and write resource data. 129 * 130 * @author Matej Knopp 131 */ 132 public static class ResourceResponse 133 { 134 private Integer errorCode; 135 private Integer statusCode; 136 private String errorMessage; 137 private String fileName = null; 138 private ContentDisposition contentDisposition = ContentDisposition.INLINE; 139 private String contentType = null; 140 private String contentRange = null; 141 private ContentRangeType contentRangeType = null; 142 private String textEncoding; 143 private long contentLength = -1; 144 private Instant lastModified = null; 145 private WriteCallback writeCallback; 146 private Duration cacheDuration; 147 private WebResponse.CacheScope cacheScope; 148 private final HttpHeaderCollection headers; 149 150 /** 151 * Construct. 152 */ 153 public ResourceResponse() 154 { 155 // disallow caching for public caches. this behavior is similar to wicket 1.4: 156 // setting it to [PUBLIC] seems to be sexy but could potentially cache confidential 157 // data on public proxies for users migrating to 1.5 158 cacheScope = WebResponse.CacheScope.PRIVATE; 159 160 // collection of directly set response headers 161 headers = new HttpHeaderCollection(); 162 } 163 164 /** 165 * Sets the error code for resource. If there is an error code set the data will not be 166 * rendered and the code will be sent to client. 167 * 168 * @param errorCode 169 * error code 170 * 171 * @return {@code this}, for chaining. 172 */ 173 public ResourceResponse setError(Integer errorCode) 174 { 175 setError(errorCode, null); 176 return this; 177 } 178 179 /** 180 * Sets the error code and message for resource. If there is an error code set the data will 181 * not be rendered and the code and message will be sent to client. 182 * 183 * @param errorCode 184 * error code 185 * @param errorMessage 186 * error message 187 * 188 * @return {@code this}, for chaining. 189 */ 190 public ResourceResponse setError(Integer errorCode, String errorMessage) 191 { 192 this.errorCode = errorCode; 193 this.errorMessage = errorMessage; 194 return this; 195 } 196 197 /** 198 * @return error code or <code>null</code> 199 */ 200 public Integer getErrorCode() 201 { 202 return errorCode; 203 } 204 205 /** 206 * Sets the status code for resource. 207 * 208 * @param statusCode 209 * status code 210 * 211 * @return {@code this}, for chaining. 212 */ 213 public ResourceResponse setStatusCode(Integer statusCode) 214 { 215 this.statusCode = statusCode; 216 return this; 217 } 218 219 /** 220 * @return status code or <code>null</code> 221 */ 222 public Integer getStatusCode() 223 { 224 return statusCode; 225 } 226 227 /** 228 * @return error message or <code>null</code> 229 */ 230 public String getErrorMessage() 231 { 232 return errorMessage; 233 } 234 235 /** 236 * Sets the file name of the resource. 237 * 238 * @param fileName 239 * file name 240 * 241 * @return {@code this}, for chaining. 242 */ 243 public ResourceResponse setFileName(String fileName) 244 { 245 this.fileName = fileName; 246 return this; 247 } 248 249 /** 250 * @return resource file name 251 */ 252 public String getFileName() 253 { 254 return fileName; 255 } 256 257 /** 258 * Determines whether the resource will be inline or an attachment. 259 * 260 * @see ContentDisposition 261 * 262 * @param contentDisposition 263 * content disposition (attachment or inline) 264 * 265 * @return {@code this}, for chaining. 266 */ 267 public ResourceResponse setContentDisposition(ContentDisposition contentDisposition) 268 { 269 Args.notNull(contentDisposition, "contentDisposition"); 270 this.contentDisposition = contentDisposition; 271 return this; 272 } 273 274 /** 275 * @return whether the resource is inline or attachment 276 */ 277 public ContentDisposition getContentDisposition() 278 { 279 return contentDisposition; 280 } 281 282 /** 283 * Sets the content type for the resource. If no content type is set it will be determined 284 * by the extension. 285 * 286 * @param contentType 287 * content type (also known as mime type) 288 * 289 * @return {@code this}, for chaining. 290 */ 291 public ResourceResponse setContentType(String contentType) 292 { 293 this.contentType = contentType; 294 return this; 295 } 296 297 /** 298 * @return resource content type 299 */ 300 public String getContentType() 301 { 302 if (contentType == null && fileName != null) 303 { 304 contentType = Application.get().getMimeType(fileName); 305 } 306 return contentType; 307 } 308 309 /** 310 * Gets the content range of the resource. If no content range is set the client assumes the 311 * whole content. 312 * 313 * @return the content range 314 */ 315 public String getContentRange() 316 { 317 return contentRange; 318 } 319 320 /** 321 * Sets the content range of the resource. If no content range is set the client assumes the 322 * whole content. Please note that if the content range is set, the content length, the 323 * status code and the accept range must be set right, too. 324 * 325 * @param contentRange 326 * the content range 327 */ 328 public void setContentRange(String contentRange) 329 { 330 this.contentRange = contentRange; 331 } 332 333 /** 334 * If the resource accepts ranges 335 * 336 * @return the type of range (e.g. bytes) 337 */ 338 public ContentRangeType getAcceptRange() 339 { 340 return contentRangeType; 341 } 342 343 /** 344 * Sets the accept range header (e.g. bytes) 345 * 346 * @param contentRangeType 347 * the content range header information 348 */ 349 public void setAcceptRange(ContentRangeType contentRangeType) 350 { 351 this.contentRangeType = contentRangeType; 352 } 353 354 /** 355 * Sets the text encoding for the resource. This setting must only used if the resource 356 * response represents text. 357 * 358 * @param textEncoding 359 * character encoding of text body 360 * 361 * @return {@code this}, for chaining. 362 */ 363 public ResourceResponse setTextEncoding(String textEncoding) 364 { 365 this.textEncoding = textEncoding; 366 return this; 367 } 368 369 /** 370 * @return text encoding for resource 371 */ 372 protected String getTextEncoding() 373 { 374 return textEncoding; 375 } 376 377 /** 378 * Sets the content length (in bytes) of the data. Content length is optional but it's 379 * recommended to set it so that the browser can show download progress. 380 * 381 * @param contentLength 382 * length of response body 383 * 384 * @return {@code this}, for chaining. 385 */ 386 public ResourceResponse setContentLength(long contentLength) 387 { 388 this.contentLength = contentLength; 389 return this; 390 } 391 392 /** 393 * @return content length (in bytes) 394 */ 395 public long getContentLength() 396 { 397 return contentLength; 398 } 399 400 /** 401 * Sets the last modified data of the resource. Even though this method is optional it is 402 * recommended to set the date. If the date is set properly Wicket can check the 403 * <code>If-Modified-Since</code> to determine if the actual data really needs to be sent 404 * to client. 405 * 406 * @param lastModified 407 * last modification timestamp 408 * 409 * @return {@code this}, for chaining. 410 */ 411 public ResourceResponse setLastModified(Instant lastModified) 412 { 413 this.lastModified = lastModified; 414 return this; 415 } 416 417 /** 418 * @return last modification timestamp 419 */ 420 public Instant getLastModified() 421 { 422 return lastModified; 423 } 424 425 /** 426 * Check to determine if the resource data needs to be written. This method checks the 427 * <code>If-Modified-Since</code> request header and compares it to lastModified property. 428 * In order for this method to work {@link #setLastModified(Instant)} has to be called first. 429 * 430 * @param attributes 431 * request attributes 432 * @return <code>true</code> if the resource data does need to be written, 433 * <code>false</code> otherwise. 434 */ 435 public boolean dataNeedsToBeWritten(Attributes attributes) 436 { 437 WebRequest request = (WebRequest)attributes.getRequest(); 438 Instant ifModifiedSince = request.getIfModifiedSinceHeader(); 439 440 if (cacheDuration != Duration.ZERO && ifModifiedSince != null && lastModified != null) 441 { 442 // [Last-Modified] headers have a maximum precision of one second 443 // so we have to truncate the milliseconds part for a proper compare. 444 // that's stupid, since changes within one second will not be reliably 445 // detected by the client ... any hint or clarification to improve this 446 // situation will be appreciated... 447 Instant roundedLastModified = lastModified.truncatedTo(ChronoUnit.SECONDS); 448 449 return ifModifiedSince.isBefore(roundedLastModified); 450 } 451 else 452 { 453 return true; 454 } 455 } 456 457 /** 458 * Disables caching. 459 * 460 * @return {@code this}, for chaining. 461 */ 462 public ResourceResponse disableCaching() 463 { 464 return setCacheDuration(Duration.ZERO); 465 } 466 467 /** 468 * Sets caching to maximum available duration. 469 * 470 * @return {@code this}, for chaining. 471 */ 472 public ResourceResponse setCacheDurationToMaximum() 473 { 474 cacheDuration = WebResponse.MAX_CACHE_DURATION; 475 return this; 476 } 477 478 /** 479 * Controls how long this response may be cached. 480 * 481 * @param duration 482 * caching duration in seconds 483 * 484 * @return {@code this}, for chaining. 485 */ 486 public ResourceResponse setCacheDuration(Duration duration) 487 { 488 cacheDuration = Args.notNull(duration, "duration"); 489 return this; 490 } 491 492 /** 493 * Returns how long this resource may be cached for. 494 * <p/> 495 * The special value Duration.NONE means caching is disabled. 496 * 497 * @return duration for caching 498 * 499 * @see org.apache.wicket.settings.ResourceSettings#setDefaultCacheDuration(Duration) 500 * @see org.apache.wicket.settings.ResourceSettings#getDefaultCacheDuration() 501 */ 502 public Duration getCacheDuration() 503 { 504 Duration duration = cacheDuration; 505 if (duration == null && Application.exists()) 506 { 507 duration = Application.get().getResourceSettings().getDefaultCacheDuration(); 508 } 509 510 return duration; 511 } 512 513 /** 514 * returns what kind of caches are allowed to cache the resource response 515 * <p/> 516 * resources are only cached at all if caching is enabled by setting a cache duration. 517 * 518 * @return cache scope 519 * 520 * @see org.apache.wicket.request.resource.AbstractResource.ResourceResponse#getCacheDuration() 521 * @see org.apache.wicket.request.resource.AbstractResource.ResourceResponse#setCacheDuration(Duration) 522 * @see org.apache.wicket.request.http.WebResponse.CacheScope 523 */ 524 public WebResponse.CacheScope getCacheScope() 525 { 526 return cacheScope; 527 } 528 529 /** 530 * controls what kind of caches are allowed to cache the response 531 * <p/> 532 * resources are only cached at all if caching is enabled by setting a cache duration. 533 * 534 * @param scope 535 * scope for caching 536 * 537 * @see org.apache.wicket.request.resource.AbstractResource.ResourceResponse#getCacheDuration() 538 * @see org.apache.wicket.request.resource.AbstractResource.ResourceResponse#setCacheDuration(Duration) 539 * @see org.apache.wicket.request.http.WebResponse.CacheScope 540 * 541 * @return {@code this}, for chaining. 542 */ 543 public ResourceResponse setCacheScope(WebResponse.CacheScope scope) 544 { 545 cacheScope = Args.notNull(scope, "scope"); 546 return this; 547 } 548 549 /** 550 * Sets the {@link WriteCallback}. The callback is responsible for generating the response 551 * data. 552 * <p> 553 * It is necessary to set the {@link WriteCallback} if 554 * {@link #dataNeedsToBeWritten(org.apache.wicket.request.resource.IResource.Attributes)} 555 * returns <code>true</code> and {@link #setError(Integer)} has not been called. 556 * 557 * @param writeCallback 558 * write callback 559 * 560 * @return {@code this}, for chaining. 561 */ 562 public ResourceResponse setWriteCallback(final WriteCallback writeCallback) 563 { 564 Args.notNull(writeCallback, "writeCallback"); 565 this.writeCallback = writeCallback; 566 return this; 567 } 568 569 /** 570 * @return write callback. 571 */ 572 public WriteCallback getWriteCallback() 573 { 574 return writeCallback; 575 } 576 577 /** 578 * get custom headers 579 * 580 * @return collection of the response headers 581 */ 582 public HttpHeaderCollection getHeaders() 583 { 584 return headers; 585 } 586 } 587 588 /** 589 * Configure the web response header for client cache control. 590 * 591 * @param data 592 * resource data 593 * @param attributes 594 * request attributes 595 */ 596 protected void configureCache(final ResourceResponse data, final Attributes attributes) 597 { 598 Response response = attributes.getResponse(); 599 600 if (response instanceof WebResponse) 601 { 602 Duration duration = data.getCacheDuration(); 603 WebResponse webResponse = (WebResponse)response; 604 if (duration.compareTo(Duration.ZERO) > 0) 605 { 606 webResponse.enableCaching(duration, data.getCacheScope()); 607 } 608 else 609 { 610 webResponse.disableCaching(); 611 } 612 } 613 } 614 615 protected IResourceCachingStrategy getCachingStrategy() 616 { 617 return Application.get().getResourceSettings().getCachingStrategy(); 618 } 619 620 /** 621 * 622 * @see org.apache.wicket.request.resource.IResource#respond(org.apache.wicket.request.resource.IResource.Attributes) 623 */ 624 @Override 625 public void respond(final Attributes attributes) 626 { 627 // Sets the request attributes 628 setRequestMetaData(attributes); 629 630 // Get a "new" ResourceResponse to write a response 631 ResourceResponse data = newResourceResponse(attributes); 632 633 // is resource supposed to be cached? 634 if (this instanceof IStaticCacheableResource) 635 { 636 final IStaticCacheableResource cacheable = (IStaticCacheableResource)this; 637 638 // is caching enabled? 639 if (cacheable.isCachingEnabled()) 640 { 641 // apply caching strategy to response 642 getCachingStrategy().decorateResponse(data, cacheable); 643 } 644 } 645 // set response header 646 setResponseHeaders(data, attributes); 647 648 if (!data.dataNeedsToBeWritten(attributes) || data.getErrorCode() != null || 649 needsBody(data.getStatusCode()) == false) 650 { 651 return; 652 } 653 654 if (data.getWriteCallback() == null) 655 { 656 throw new IllegalStateException("ResourceResponse#setWriteCallback() must be set."); 657 } 658 659 try 660 { 661 data.getWriteCallback().writeData(attributes); 662 } 663 catch (IOException iox) 664 { 665 throw new WicketRuntimeException(iox); 666 } 667 } 668 669 /** 670 * Decides whether a response body should be written back to the client depending on the set 671 * status code 672 * 673 * @param statusCode 674 * the status code set by the application 675 * @return {@code true} if the status code allows response body, {@code false} - otherwise 676 */ 677 private boolean needsBody(Integer statusCode) 678 { 679 return statusCode == null || 680 (statusCode < 300 && 681 statusCode != HttpServletResponse.SC_NO_CONTENT && 682 statusCode != HttpServletResponse.SC_RESET_CONTENT); 683 } 684 685 /** 686 * check if header is directly modifyable 687 * 688 * @param name 689 * header name 690 * 691 * @throws IllegalArgumentException 692 * if access is forbidden 693 */ 694 private void checkHeaderAccess(String name) 695 { 696 name = Args.notEmpty(name.trim().toLowerCase(Locale.ROOT), "name"); 697 698 if (INTERNAL_HEADERS.contains(name)) 699 { 700 throw new IllegalArgumentException("you are not allowed to directly access header [" + 701 name + "], " + "use one of the other specialized methods of " + 702 Classes.simpleName(getClass()) + " to get or modify its value"); 703 } 704 } 705 706 /** 707 * Reads the plain request header information and applies enriched information as meta data to 708 * the current request. Those information are available for the whole request cycle. 709 * 710 * @param attributes 711 * the attributes to get the plain request header information 712 */ 713 protected void setRequestMetaData(Attributes attributes) 714 { 715 Request request = attributes.getRequest(); 716 if (request instanceof WebRequest) 717 { 718 WebRequest webRequest = (WebRequest)request; 719 720 setRequestRangeMetaData(webRequest); 721 } 722 } 723 724 protected void setRequestRangeMetaData(WebRequest webRequest) 725 { 726 String rangeHeader = webRequest.getHeader("range"); 727 728 // The content range header is only be calculated if a range is given 729 if (!Strings.isEmpty(rangeHeader) && 730 rangeHeader.contains(ContentRangeType.BYTES.getTypeName())) 731 { 732 // fixing white spaces 733 rangeHeader = rangeHeader.replaceAll(" ", ""); 734 735 String range = rangeHeader.substring(rangeHeader.indexOf('=') + 1, 736 rangeHeader.length()); 737 738 // support only the first range (WICKET-5995) 739 final int idxOfComma = range.indexOf(','); 740 String firstRange = idxOfComma > -1 ? range.substring(0, idxOfComma) : range; 741 742 String[] rangeParts = Strings.split(firstRange, '-'); 743 744 String startByteString = rangeParts[0]; 745 String endByteString = rangeParts[1]; 746 747 long startbyte = !Strings.isEmpty(startByteString) ? Long.parseLong(startByteString) : 0; 748 long endbyte = !Strings.isEmpty(endByteString) ? Long.parseLong(endByteString) : -1; 749 750 // Make the content range information available for the whole request cycle 751 RequestCycle requestCycle = RequestCycle.get(); 752 requestCycle.setMetaData(CONTENT_RANGE_STARTBYTE, startbyte); 753 requestCycle.setMetaData(CONTENT_RANGE_ENDBYTE, endbyte); 754 } 755 } 756 757 /** 758 * Sets the response header of resource response to the response received from the attributes 759 * 760 * @param resourceResponse 761 * the resource response to get the header fields from 762 * @param attributes 763 * the attributes to get the response from to which the header information are going 764 * to be applied 765 */ 766 protected void setResponseHeaders(final ResourceResponse resourceResponse, 767 final Attributes attributes) 768 { 769 Response response = attributes.getResponse(); 770 if (response instanceof WebResponse) 771 { 772 WebResponse webResponse = (WebResponse)response; 773 774 // 1. Last Modified 775 Instant lastModified = resourceResponse.getLastModified(); 776 if (lastModified != null) 777 { 778 webResponse.setLastModifiedTime(lastModified); 779 } 780 781 // 2. Caching 782 configureCache(resourceResponse, attributes); 783 784 if (resourceResponse.getErrorCode() != null) 785 { 786 webResponse.sendError(resourceResponse.getErrorCode(), 787 resourceResponse.getErrorMessage()); 788 return; 789 } 790 791 if (resourceResponse.getStatusCode() != null) 792 { 793 webResponse.setStatus(resourceResponse.getStatusCode()); 794 } 795 796 if (!resourceResponse.dataNeedsToBeWritten(attributes)) 797 { 798 webResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED); 799 return; 800 } 801 802 // 3. Content Disposition 803 String fileName = resourceResponse.getFileName(); 804 ContentDisposition disposition = resourceResponse.getContentDisposition(); 805 if (ContentDisposition.ATTACHMENT == disposition) 806 { 807 webResponse.setAttachmentHeader(fileName); 808 } 809 else if (ContentDisposition.INLINE == disposition) 810 { 811 webResponse.setInlineHeader(fileName); 812 } 813 814 // 4. Mime Type (+ encoding) 815 String mimeType = resourceResponse.getContentType(); 816 if (mimeType != null) 817 { 818 final String encoding = resourceResponse.getTextEncoding(); 819 820 if (encoding == null) 821 { 822 webResponse.setContentType(mimeType); 823 } 824 else 825 { 826 webResponse.setContentType(mimeType + "; charset=" + encoding); 827 } 828 } 829 830 // 5. Accept Range 831 ContentRangeType acceptRange = resourceResponse.getAcceptRange(); 832 if (acceptRange != null) 833 { 834 webResponse.setAcceptRange(acceptRange.getTypeName()); 835 } 836 837 long contentLength = resourceResponse.getContentLength(); 838 boolean contentRangeApplied = false; 839 840 // 6. Content Range 841 // for more information take a look here: 842 // http://stackoverflow.com/questions/8293687/sample-http-range-request-session 843 // if the content range header has been set directly 844 // to the resource response use it otherwise calculate it 845 String contentRange = resourceResponse.getContentRange(); 846 if (contentRange != null) 847 { 848 webResponse.setContentRange(contentRange); 849 } 850 else 851 { 852 // content length has to be set otherwise the content range header can not be 853 // calculated - accept range must be set to bytes - others are not supported at the 854 // moment 855 if (contentLength != -1 && ContentRangeType.BYTES.equals(acceptRange)) 856 { 857 contentRangeApplied = setResponseContentRangeHeaderFields(webResponse, 858 attributes, contentLength); 859 } 860 } 861 862 // 7. Content Length 863 if (contentLength != -1 && !contentRangeApplied) 864 { 865 webResponse.setContentLength(contentLength); 866 } 867 868 // add custom headers and values 869 final HttpHeaderCollection headers = resourceResponse.getHeaders(); 870 871 for (String name : headers.getHeaderNames()) 872 { 873 checkHeaderAccess(name); 874 875 for (String value : headers.getHeaderValues(name)) 876 { 877 webResponse.addHeader(name, value); 878 } 879 } 880 } 881 } 882 883 /** 884 * Sets the content range header fields to the given web response 885 * 886 * @param webResponse 887 * the web response to apply the content range information to 888 * @param attributes 889 * the attributes to get the request from 890 * @param contentLength 891 * the content length of the response 892 * @return if the content range header information has been applied 893 */ 894 protected boolean setResponseContentRangeHeaderFields(WebResponse webResponse, 895 Attributes attributes, long contentLength) 896 { 897 boolean contentRangeApplied = false; 898 if (attributes.getRequest() instanceof WebRequest) 899 { 900 Long startbyte = RequestCycle.get().getMetaData(CONTENT_RANGE_STARTBYTE); 901 Long endbyte = RequestCycle.get().getMetaData(CONTENT_RANGE_ENDBYTE); 902 903 if (startbyte != null && endbyte != null) 904 { 905 // if end byte hasn't been set 906 if (endbyte == -1) 907 { 908 endbyte = contentLength - 1; 909 } 910 911 // Change the status code to 206 partial content 912 webResponse.setStatus(206); 913 // currently only bytes are supported. 914 webResponse.setContentRange(ContentRangeType.BYTES.getTypeName() + " " + startbyte + 915 '-' + endbyte + '/' + contentLength); 916 // WARNING - DO NOT SET THE CONTENT LENGTH, even if it is calculated right - 917 // SAFARI / CHROME are causing issues otherwise! 918 // webResponse.setContentLength((endbyte - startbyte) + 1); 919 920 // content range has been applied do not set the content length again! 921 contentRangeApplied = true; 922 } 923 } 924 return contentRangeApplied; 925 } 926 927 /** 928 * Callback invoked when resource data needs to be written to response. Subclass needs to 929 * implement the {@link #writeData(org.apache.wicket.request.resource.IResource.Attributes)} 930 * method. 931 * 932 * @author Matej Knopp 933 */ 934 public abstract static class WriteCallback 935 { 936 /** 937 * Write the resource data to response. 938 * 939 * @param attributes 940 * request attributes 941 */ 942 public abstract void writeData(Attributes attributes) throws IOException; 943 944 /** 945 * Convenience method to write an {@link InputStream} to response. 946 * 947 * @param attributes 948 * request attributes 949 * @param stream 950 * input stream 951 */ 952 protected void writeStream(Attributes attributes, InputStream stream) throws IOException 953 { 954 final Response response = attributes.getResponse(); 955 Streams.copy(stream, response.getOutputStream()); 956 } 957 } 958}