| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.wicket.request.resource; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.sql.Time; |
| import java.time.Duration; |
| import java.time.Instant; |
| import java.time.temporal.ChronoUnit; |
| import java.util.HashSet; |
| import java.util.Locale; |
| import java.util.Set; |
| |
| import jakarta.servlet.http.HttpServletResponse; |
| |
| import org.apache.wicket.Application; |
| import org.apache.wicket.MetaDataKey; |
| import org.apache.wicket.WicketRuntimeException; |
| import org.apache.wicket.request.HttpHeaderCollection; |
| import org.apache.wicket.request.Request; |
| import org.apache.wicket.request.Response; |
| import org.apache.wicket.request.cycle.RequestCycle; |
| import org.apache.wicket.request.http.WebRequest; |
| import org.apache.wicket.request.http.WebResponse; |
| import org.apache.wicket.request.resource.caching.IResourceCachingStrategy; |
| import org.apache.wicket.request.resource.caching.IStaticCacheableResource; |
| import org.apache.wicket.util.io.Streams; |
| import org.apache.wicket.util.lang.Args; |
| import org.apache.wicket.util.lang.Classes; |
| import org.apache.wicket.util.string.Strings; |
| |
| /** |
| * Convenience resource implementation. The subclass must implement |
| * {@link #newResourceResponse(org.apache.wicket.request.resource.IResource.Attributes)} method. |
| * |
| * @author Matej Knopp |
| * @author Tobias Soloschenko |
| */ |
| public abstract class AbstractResource implements IResource |
| { |
| private static final long serialVersionUID = 1L; |
| |
| /** header values that are managed internally and must not be set directly */ |
| public static final Set<String> INTERNAL_HEADERS; |
| |
| /** The meta data key of the content range start byte **/ |
| public static final MetaDataKey<Long> CONTENT_RANGE_STARTBYTE = new MetaDataKey<>() |
| { |
| private static final long serialVersionUID = 1L; |
| }; |
| |
| /** The meta data key of the content range end byte **/ |
| public static final MetaDataKey<Long> CONTENT_RANGE_ENDBYTE = new MetaDataKey<>() |
| { |
| private static final long serialVersionUID = 1L; |
| }; |
| |
| public static final String CONTENT_DISPOSITION_HEADER_NAME = "content-disposition"; |
| |
| /** |
| * All available content range types. The type name represents the name used in header |
| * information. |
| */ |
| public enum ContentRangeType |
| { |
| BYTES("bytes"), NONE("none"); |
| |
| private final String typeName; |
| |
| ContentRangeType(String typeName) |
| { |
| this.typeName = typeName; |
| } |
| |
| public String getTypeName() |
| { |
| return typeName; |
| } |
| } |
| |
| static |
| { |
| INTERNAL_HEADERS = new HashSet<>(); |
| INTERNAL_HEADERS.add("server"); |
| INTERNAL_HEADERS.add("date"); |
| INTERNAL_HEADERS.add("expires"); |
| INTERNAL_HEADERS.add("last-modified"); |
| INTERNAL_HEADERS.add("content-type"); |
| INTERNAL_HEADERS.add("content-length"); |
| INTERNAL_HEADERS.add(CONTENT_DISPOSITION_HEADER_NAME); |
| INTERNAL_HEADERS.add("transfer-encoding"); |
| INTERNAL_HEADERS.add("connection"); |
| INTERNAL_HEADERS.add("content-range"); |
| INTERNAL_HEADERS.add("accept-range"); |
| } |
| |
| /** |
| * Construct. |
| */ |
| public AbstractResource() |
| { |
| } |
| |
| /** |
| * Override this method to return a {@link ResourceResponse} for the request. |
| * |
| * @param attributes |
| * request attributes |
| * @return resource data instance |
| */ |
| protected abstract ResourceResponse newResourceResponse(Attributes attributes); |
| |
| /** |
| * Represents data used to configure response and write resource data. |
| * |
| * @author Matej Knopp |
| */ |
| public static class ResourceResponse |
| { |
| private Integer errorCode; |
| private Integer statusCode; |
| private String errorMessage; |
| private String fileName = null; |
| private ContentDisposition contentDisposition = ContentDisposition.INLINE; |
| private String contentType = null; |
| private String contentRange = null; |
| private ContentRangeType contentRangeType = null; |
| private String textEncoding; |
| private long contentLength = -1; |
| private Instant lastModified = null; |
| private WriteCallback writeCallback; |
| private Duration cacheDuration; |
| private WebResponse.CacheScope cacheScope; |
| private final HttpHeaderCollection headers; |
| |
| /** |
| * Construct. |
| */ |
| public ResourceResponse() |
| { |
| // disallow caching for public caches. this behavior is similar to wicket 1.4: |
| // setting it to [PUBLIC] seems to be sexy but could potentially cache confidential |
| // data on public proxies for users migrating to 1.5 |
| cacheScope = WebResponse.CacheScope.PRIVATE; |
| |
| // collection of directly set response headers |
| headers = new HttpHeaderCollection(); |
| } |
| |
| /** |
| * Sets the error code for resource. If there is an error code set the data will not be |
| * rendered and the code will be sent to client. |
| * |
| * @param errorCode |
| * error code |
| * |
| * @return {@code this}, for chaining. |
| */ |
| public ResourceResponse setError(Integer errorCode) |
| { |
| setError(errorCode, null); |
| return this; |
| } |
| |
| /** |
| * Sets the error code and message for resource. If there is an error code set the data will |
| * not be rendered and the code and message will be sent to client. |
| * |
| * @param errorCode |
| * error code |
| * @param errorMessage |
| * error message |
| * |
| * @return {@code this}, for chaining. |
| */ |
| public ResourceResponse setError(Integer errorCode, String errorMessage) |
| { |
| this.errorCode = errorCode; |
| this.errorMessage = errorMessage; |
| return this; |
| } |
| |
| /** |
| * @return error code or <code>null</code> |
| */ |
| public Integer getErrorCode() |
| { |
| return errorCode; |
| } |
| |
| /** |
| * Sets the status code for resource. |
| * |
| * @param statusCode |
| * status code |
| * |
| * @return {@code this}, for chaining. |
| */ |
| public ResourceResponse setStatusCode(Integer statusCode) |
| { |
| this.statusCode = statusCode; |
| return this; |
| } |
| |
| /** |
| * @return status code or <code>null</code> |
| */ |
| public Integer getStatusCode() |
| { |
| return statusCode; |
| } |
| |
| /** |
| * @return error message or <code>null</code> |
| */ |
| public String getErrorMessage() |
| { |
| return errorMessage; |
| } |
| |
| /** |
| * Sets the file name of the resource. |
| * |
| * @param fileName |
| * file name |
| * |
| * @return {@code this}, for chaining. |
| */ |
| public ResourceResponse setFileName(String fileName) |
| { |
| this.fileName = fileName; |
| return this; |
| } |
| |
| /** |
| * @return resource file name |
| */ |
| public String getFileName() |
| { |
| return fileName; |
| } |
| |
| /** |
| * Determines whether the resource will be inline or an attachment. |
| * |
| * @see ContentDisposition |
| * |
| * @param contentDisposition |
| * content disposition (attachment or inline) |
| * |
| * @return {@code this}, for chaining. |
| */ |
| public ResourceResponse setContentDisposition(ContentDisposition contentDisposition) |
| { |
| Args.notNull(contentDisposition, "contentDisposition"); |
| this.contentDisposition = contentDisposition; |
| return this; |
| } |
| |
| /** |
| * @return whether the resource is inline or attachment |
| */ |
| public ContentDisposition getContentDisposition() |
| { |
| return contentDisposition; |
| } |
| |
| /** |
| * Sets the content type for the resource. If no content type is set it will be determined |
| * by the extension. |
| * |
| * @param contentType |
| * content type (also known as mime type) |
| * |
| * @return {@code this}, for chaining. |
| */ |
| public ResourceResponse setContentType(String contentType) |
| { |
| this.contentType = contentType; |
| return this; |
| } |
| |
| /** |
| * @return resource content type |
| */ |
| public String getContentType() |
| { |
| if (contentType == null && fileName != null) |
| { |
| contentType = Application.get().getMimeType(fileName); |
| } |
| return contentType; |
| } |
| |
| /** |
| * Gets the content range of the resource. If no content range is set the client assumes the |
| * whole content. |
| * |
| * @return the content range |
| */ |
| public String getContentRange() |
| { |
| return contentRange; |
| } |
| |
| /** |
| * Sets the content range of the resource. If no content range is set the client assumes the |
| * whole content. Please note that if the content range is set, the content length, the |
| * status code and the accept range must be set right, too. |
| * |
| * @param contentRange |
| * the content range |
| */ |
| public void setContentRange(String contentRange) |
| { |
| this.contentRange = contentRange; |
| } |
| |
| /** |
| * If the resource accepts ranges |
| * |
| * @return the type of range (e.g. bytes) |
| */ |
| public ContentRangeType getAcceptRange() |
| { |
| return contentRangeType; |
| } |
| |
| /** |
| * Sets the accept range header (e.g. bytes) |
| * |
| * @param contentRangeType |
| * the content range header information |
| */ |
| public void setAcceptRange(ContentRangeType contentRangeType) |
| { |
| this.contentRangeType = contentRangeType; |
| } |
| |
| /** |
| * Sets the text encoding for the resource. This setting must only used if the resource |
| * response represents text. |
| * |
| * @param textEncoding |
| * character encoding of text body |
| * |
| * @return {@code this}, for chaining. |
| */ |
| public ResourceResponse setTextEncoding(String textEncoding) |
| { |
| this.textEncoding = textEncoding; |
| return this; |
| } |
| |
| /** |
| * @return text encoding for resource |
| */ |
| protected String getTextEncoding() |
| { |
| return textEncoding; |
| } |
| |
| /** |
| * Sets the content length (in bytes) of the data. Content length is optional but it's |
| * recommended to set it so that the browser can show download progress. |
| * |
| * @param contentLength |
| * length of response body |
| * |
| * @return {@code this}, for chaining. |
| */ |
| public ResourceResponse setContentLength(long contentLength) |
| { |
| this.contentLength = contentLength; |
| return this; |
| } |
| |
| /** |
| * @return content length (in bytes) |
| */ |
| public long getContentLength() |
| { |
| return contentLength; |
| } |
| |
| /** |
| * Sets the last modified data of the resource. Even though this method is optional it is |
| * recommended to set the date. If the date is set properly Wicket can check the |
| * <code>If-Modified-Since</code> to determine if the actual data really needs to be sent |
| * to client. |
| * |
| * @param lastModified |
| * last modification timestamp |
| * |
| * @return {@code this}, for chaining. |
| */ |
| public ResourceResponse setLastModified(Instant lastModified) |
| { |
| this.lastModified = lastModified; |
| return this; |
| } |
| |
| /** |
| * @return last modification timestamp |
| */ |
| public Instant getLastModified() |
| { |
| return lastModified; |
| } |
| |
| /** |
| * Check to determine if the resource data needs to be written. This method checks the |
| * <code>If-Modified-Since</code> request header and compares it to lastModified property. |
| * In order for this method to work {@link #setLastModified(Time)} has to be called first. |
| * |
| * @param attributes |
| * request attributes |
| * @return <code>true</code> if the resource data does need to be written, |
| * <code>false</code> otherwise. |
| */ |
| public boolean dataNeedsToBeWritten(Attributes attributes) |
| { |
| WebRequest request = (WebRequest)attributes.getRequest(); |
| Instant ifModifiedSince = request.getIfModifiedSinceHeader(); |
| |
| if (cacheDuration != Duration.ZERO && ifModifiedSince != null && lastModified != null) |
| { |
| // [Last-Modified] headers have a maximum precision of one second |
| // so we have to truncate the milliseconds part for a proper compare. |
| // that's stupid, since changes within one second will not be reliably |
| // detected by the client ... any hint or clarification to improve this |
| // situation will be appreciated... |
| Instant roundedLastModified = lastModified.truncatedTo(ChronoUnit.SECONDS); |
| |
| return ifModifiedSince.isBefore(roundedLastModified); |
| } |
| else |
| { |
| return true; |
| } |
| } |
| |
| /** |
| * Disables caching. |
| * |
| * @return {@code this}, for chaining. |
| */ |
| public ResourceResponse disableCaching() |
| { |
| return setCacheDuration(Duration.ZERO); |
| } |
| |
| /** |
| * Sets caching to maximum available duration. |
| * |
| * @return {@code this}, for chaining. |
| */ |
| public ResourceResponse setCacheDurationToMaximum() |
| { |
| cacheDuration = WebResponse.MAX_CACHE_DURATION; |
| return this; |
| } |
| |
| /** |
| * Controls how long this response may be cached. |
| * |
| * @param duration |
| * caching duration in seconds |
| * |
| * @return {@code this}, for chaining. |
| */ |
| public ResourceResponse setCacheDuration(Duration duration) |
| { |
| cacheDuration = Args.notNull(duration, "duration"); |
| return this; |
| } |
| |
| /** |
| * Returns how long this resource may be cached for. |
| * <p/> |
| * The special value Duration.NONE means caching is disabled. |
| * |
| * @return duration for caching |
| * |
| * @see org.apache.wicket.settings.ResourceSettings#setDefaultCacheDuration(org.apache.wicket.util.time.Duration) |
| * @see org.apache.wicket.settings.ResourceSettings#getDefaultCacheDuration() |
| */ |
| public Duration getCacheDuration() |
| { |
| Duration duration = cacheDuration; |
| if (duration == null && Application.exists()) |
| { |
| duration = Application.get().getResourceSettings().getDefaultCacheDuration(); |
| } |
| |
| return duration; |
| } |
| |
| /** |
| * returns what kind of caches are allowed to cache the resource response |
| * <p/> |
| * resources are only cached at all if caching is enabled by setting a cache duration. |
| * |
| * @return cache scope |
| * |
| * @see org.apache.wicket.request.resource.AbstractResource.ResourceResponse#getCacheDuration() |
| * @see org.apache.wicket.request.resource.AbstractResource.ResourceResponse#setCacheDuration(org.apache.wicket.util.time.Duration) |
| * @see org.apache.wicket.request.http.WebResponse.CacheScope |
| */ |
| public WebResponse.CacheScope getCacheScope() |
| { |
| return cacheScope; |
| } |
| |
| /** |
| * controls what kind of caches are allowed to cache the response |
| * <p/> |
| * resources are only cached at all if caching is enabled by setting a cache duration. |
| * |
| * @param scope |
| * scope for caching |
| * |
| * @see org.apache.wicket.request.resource.AbstractResource.ResourceResponse#getCacheDuration() |
| * @see org.apache.wicket.request.resource.AbstractResource.ResourceResponse#setCacheDuration(org.apache.wicket.util.time.Duration) |
| * @see org.apache.wicket.request.http.WebResponse.CacheScope |
| * |
| * @return {@code this}, for chaining. |
| */ |
| public ResourceResponse setCacheScope(WebResponse.CacheScope scope) |
| { |
| cacheScope = Args.notNull(scope, "scope"); |
| return this; |
| } |
| |
| /** |
| * Sets the {@link WriteCallback}. The callback is responsible for generating the response |
| * data. |
| * <p> |
| * It is necessary to set the {@link WriteCallback} if |
| * {@link #dataNeedsToBeWritten(org.apache.wicket.request.resource.IResource.Attributes)} |
| * returns <code>true</code> and {@link #setError(Integer)} has not been called. |
| * |
| * @param writeCallback |
| * write callback |
| * |
| * @return {@code this}, for chaining. |
| */ |
| public ResourceResponse setWriteCallback(final WriteCallback writeCallback) |
| { |
| Args.notNull(writeCallback, "writeCallback"); |
| this.writeCallback = writeCallback; |
| return this; |
| } |
| |
| /** |
| * @return write callback. |
| */ |
| public WriteCallback getWriteCallback() |
| { |
| return writeCallback; |
| } |
| |
| /** |
| * get custom headers |
| * |
| * @return collection of the response headers |
| */ |
| public HttpHeaderCollection getHeaders() |
| { |
| return headers; |
| } |
| } |
| |
| /** |
| * Configure the web response header for client cache control. |
| * |
| * @param data |
| * resource data |
| * @param attributes |
| * request attributes |
| */ |
| protected void configureCache(final ResourceResponse data, final Attributes attributes) |
| { |
| Response response = attributes.getResponse(); |
| |
| if (response instanceof WebResponse) |
| { |
| Duration duration = data.getCacheDuration(); |
| WebResponse webResponse = (WebResponse)response; |
| if (duration.compareTo(Duration.ZERO) > 0) |
| { |
| webResponse.enableCaching(duration, data.getCacheScope()); |
| } |
| else |
| { |
| webResponse.disableCaching(); |
| } |
| } |
| } |
| |
| protected IResourceCachingStrategy getCachingStrategy() |
| { |
| return Application.get().getResourceSettings().getCachingStrategy(); |
| } |
| |
| /** |
| * |
| * @see org.apache.wicket.request.resource.IResource#respond(org.apache.wicket.request.resource.IResource.Attributes) |
| */ |
| @Override |
| public void respond(final Attributes attributes) |
| { |
| // Sets the request attributes |
| setRequestMetaData(attributes); |
| |
| // Get a "new" ResourceResponse to write a response |
| ResourceResponse data = newResourceResponse(attributes); |
| |
| // is resource supposed to be cached? |
| if (this instanceof IStaticCacheableResource) |
| { |
| final IStaticCacheableResource cacheable = (IStaticCacheableResource)this; |
| |
| // is caching enabled? |
| if (cacheable.isCachingEnabled()) |
| { |
| // apply caching strategy to response |
| getCachingStrategy().decorateResponse(data, cacheable); |
| } |
| } |
| // set response header |
| setResponseHeaders(data, attributes); |
| |
| if (!data.dataNeedsToBeWritten(attributes) || data.getErrorCode() != null || |
| needsBody(data.getStatusCode()) == false) |
| { |
| return; |
| } |
| |
| if (data.getWriteCallback() == null) |
| { |
| throw new IllegalStateException("ResourceResponse#setWriteCallback() must be set."); |
| } |
| |
| try |
| { |
| data.getWriteCallback().writeData(attributes); |
| } |
| catch (IOException iox) |
| { |
| throw new WicketRuntimeException(iox); |
| } |
| } |
| |
| /** |
| * Decides whether a response body should be written back to the client depending on the set |
| * status code |
| * |
| * @param statusCode |
| * the status code set by the application |
| * @return {@code true} if the status code allows response body, {@code false} - otherwise |
| */ |
| private boolean needsBody(Integer statusCode) |
| { |
| return statusCode == null || |
| (statusCode < 300 && |
| statusCode != HttpServletResponse.SC_NO_CONTENT && |
| statusCode != HttpServletResponse.SC_RESET_CONTENT); |
| } |
| |
| /** |
| * check if header is directly modifyable |
| * |
| * @param name |
| * header name |
| * |
| * @throws IllegalArgumentException |
| * if access is forbidden |
| */ |
| private void checkHeaderAccess(String name) |
| { |
| name = Args.notEmpty(name.trim().toLowerCase(Locale.ROOT), "name"); |
| |
| if (INTERNAL_HEADERS.contains(name)) |
| { |
| throw new IllegalArgumentException("you are not allowed to directly access header [" + |
| name + "], " + "use one of the other specialized methods of " + |
| Classes.simpleName(getClass()) + " to get or modify its value"); |
| } |
| } |
| |
| /** |
| * Reads the plain request header information and applies enriched information as meta data to |
| * the current request. Those information are available for the whole request cycle. |
| * |
| * @param attributes |
| * the attributes to get the plain request header information |
| */ |
| protected void setRequestMetaData(Attributes attributes) |
| { |
| Request request = attributes.getRequest(); |
| if (request instanceof WebRequest) |
| { |
| WebRequest webRequest = (WebRequest)request; |
| |
| setRequestRangeMetaData(webRequest); |
| } |
| } |
| |
| protected void setRequestRangeMetaData(WebRequest webRequest) |
| { |
| String rangeHeader = webRequest.getHeader("range"); |
| |
| // The content range header is only be calculated if a range is given |
| if (!Strings.isEmpty(rangeHeader) && |
| rangeHeader.contains(ContentRangeType.BYTES.getTypeName())) |
| { |
| // fixing white spaces |
| rangeHeader = rangeHeader.replaceAll(" ", ""); |
| |
| String range = rangeHeader.substring(rangeHeader.indexOf('=') + 1, |
| rangeHeader.length()); |
| |
| // support only the first range (WICKET-5995) |
| final int idxOfComma = range.indexOf(','); |
| String firstRange = idxOfComma > -1 ? range.substring(0, idxOfComma) : range; |
| |
| String[] rangeParts = Strings.split(firstRange, '-'); |
| |
| String startByteString = rangeParts[0]; |
| String endByteString = rangeParts[1]; |
| |
| long startbyte = !Strings.isEmpty(startByteString) ? Long.parseLong(startByteString) : 0; |
| long endbyte = !Strings.isEmpty(endByteString) ? Long.parseLong(endByteString) : -1; |
| |
| // Make the content range information available for the whole request cycle |
| RequestCycle requestCycle = RequestCycle.get(); |
| requestCycle.setMetaData(CONTENT_RANGE_STARTBYTE, startbyte); |
| requestCycle.setMetaData(CONTENT_RANGE_ENDBYTE, endbyte); |
| } |
| } |
| |
| /** |
| * Sets the response header of resource response to the response received from the attributes |
| * |
| * @param resourceResponse |
| * the resource response to get the header fields from |
| * @param attributes |
| * the attributes to get the response from to which the header information are going |
| * to be applied |
| */ |
| protected void setResponseHeaders(final ResourceResponse resourceResponse, |
| final Attributes attributes) |
| { |
| Response response = attributes.getResponse(); |
| if (response instanceof WebResponse) |
| { |
| WebResponse webResponse = (WebResponse)response; |
| |
| // 1. Last Modified |
| Instant lastModified = resourceResponse.getLastModified(); |
| if (lastModified != null) |
| { |
| webResponse.setLastModifiedTime(lastModified); |
| } |
| |
| // 2. Caching |
| configureCache(resourceResponse, attributes); |
| |
| if (resourceResponse.getErrorCode() != null) |
| { |
| webResponse.sendError(resourceResponse.getErrorCode(), |
| resourceResponse.getErrorMessage()); |
| return; |
| } |
| |
| if (resourceResponse.getStatusCode() != null) |
| { |
| webResponse.setStatus(resourceResponse.getStatusCode()); |
| } |
| |
| if (!resourceResponse.dataNeedsToBeWritten(attributes)) |
| { |
| webResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED); |
| return; |
| } |
| |
| // 3. Content Disposition |
| String fileName = resourceResponse.getFileName(); |
| ContentDisposition disposition = resourceResponse.getContentDisposition(); |
| if (ContentDisposition.ATTACHMENT == disposition) |
| { |
| webResponse.setAttachmentHeader(fileName); |
| } |
| else if (ContentDisposition.INLINE == disposition) |
| { |
| webResponse.setInlineHeader(fileName); |
| } |
| |
| // 4. Mime Type (+ encoding) |
| String mimeType = resourceResponse.getContentType(); |
| if (mimeType != null) |
| { |
| final String encoding = resourceResponse.getTextEncoding(); |
| |
| if (encoding == null) |
| { |
| webResponse.setContentType(mimeType); |
| } |
| else |
| { |
| webResponse.setContentType(mimeType + "; charset=" + encoding); |
| } |
| } |
| |
| // 5. Accept Range |
| ContentRangeType acceptRange = resourceResponse.getAcceptRange(); |
| if (acceptRange != null) |
| { |
| webResponse.setAcceptRange(acceptRange.getTypeName()); |
| } |
| |
| long contentLength = resourceResponse.getContentLength(); |
| boolean contentRangeApplied = false; |
| |
| // 6. Content Range |
| // for more information take a look here: |
| // http://stackoverflow.com/questions/8293687/sample-http-range-request-session |
| // if the content range header has been set directly |
| // to the resource response use it otherwise calculate it |
| String contentRange = resourceResponse.getContentRange(); |
| if (contentRange != null) |
| { |
| webResponse.setContentRange(contentRange); |
| } |
| else |
| { |
| // content length has to be set otherwise the content range header can not be |
| // calculated - accept range must be set to bytes - others are not supported at the |
| // moment |
| if (contentLength != -1 && ContentRangeType.BYTES.equals(acceptRange)) |
| { |
| contentRangeApplied = setResponseContentRangeHeaderFields(webResponse, |
| attributes, contentLength); |
| } |
| } |
| |
| // 7. Content Length |
| if (contentLength != -1 && !contentRangeApplied) |
| { |
| webResponse.setContentLength(contentLength); |
| } |
| |
| // add custom headers and values |
| final HttpHeaderCollection headers = resourceResponse.getHeaders(); |
| |
| for (String name : headers.getHeaderNames()) |
| { |
| checkHeaderAccess(name); |
| |
| for (String value : headers.getHeaderValues(name)) |
| { |
| webResponse.addHeader(name, value); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Sets the content range header fields to the given web response |
| * |
| * @param webResponse |
| * the web response to apply the content range information to |
| * @param attributes |
| * the attributes to get the request from |
| * @param contentLength |
| * the content length of the response |
| * @return if the content range header information has been applied |
| */ |
| protected boolean setResponseContentRangeHeaderFields(WebResponse webResponse, |
| Attributes attributes, long contentLength) |
| { |
| boolean contentRangeApplied = false; |
| if (attributes.getRequest() instanceof WebRequest) |
| { |
| Long startbyte = RequestCycle.get().getMetaData(CONTENT_RANGE_STARTBYTE); |
| Long endbyte = RequestCycle.get().getMetaData(CONTENT_RANGE_ENDBYTE); |
| |
| if (startbyte != null && endbyte != null) |
| { |
| // if end byte hasn't been set |
| if (endbyte == -1) |
| { |
| endbyte = contentLength - 1; |
| } |
| |
| // Change the status code to 206 partial content |
| webResponse.setStatus(206); |
| // currently only bytes are supported. |
| webResponse.setContentRange(ContentRangeType.BYTES.getTypeName() + " " + startbyte + |
| '-' + endbyte + '/' + contentLength); |
| // WARNING - DO NOT SET THE CONTENT LENGTH, even if it is calculated right - |
| // SAFARI / CHROME are causing issues otherwise! |
| // webResponse.setContentLength((endbyte - startbyte) + 1); |
| |
| // content range has been applied do not set the content length again! |
| contentRangeApplied = true; |
| } |
| } |
| return contentRangeApplied; |
| } |
| |
| /** |
| * Callback invoked when resource data needs to be written to response. Subclass needs to |
| * implement the {@link #writeData(org.apache.wicket.request.resource.IResource.Attributes)} |
| * method. |
| * |
| * @author Matej Knopp |
| */ |
| public abstract static class WriteCallback |
| { |
| /** |
| * Write the resource data to response. |
| * |
| * @param attributes |
| * request attributes |
| */ |
| public abstract void writeData(Attributes attributes) throws IOException; |
| |
| /** |
| * Convenience method to write an {@link InputStream} to response. |
| * |
| * @param attributes |
| * request attributes |
| * @param stream |
| * input stream |
| */ |
| protected final void writeStream(Attributes attributes, InputStream stream) throws IOException |
| { |
| final Response response = attributes.getResponse(); |
| Streams.copy(stream, response.getOutputStream()); |
| } |
| } |
| } |