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 javax.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}