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.http;
018
019import java.io.IOException;
020import java.nio.charset.StandardCharsets;
021import java.time.Duration;
022import java.time.Instant;
023import javax.servlet.http.Cookie;
024import org.apache.wicket.request.Response;
025import org.apache.wicket.util.encoding.UrlEncoder;
026import org.apache.wicket.util.lang.Args;
027import org.apache.wicket.util.string.Strings;
028
029/**
030 * Base class for web-related responses.
031 * 
032 * @author Matej Knopp
033 */
034public abstract class WebResponse extends Response
035{
036        /** Recommended value for cache duration */
037        // one year, maximum recommended cache duration in RFC-2616
038        public static final Duration MAX_CACHE_DURATION = Duration.ofDays(365);
039
040        /**
041         * Add a cookie to the web response
042         * 
043         * @param cookie
044         */
045        public abstract void addCookie(final Cookie cookie);
046
047        /**
048         * Convenience method for clearing a cookie.
049         * 
050         * @param cookie
051         *            The cookie to set
052         * @see WebResponse#addCookie(Cookie)
053         */
054        public abstract void clearCookie(final Cookie cookie);
055
056        /**
057         * Indicates if the response supports setting headers. When this method returns
058         * false, {@link #setHeader(String, String)} and its variations will thrown an
059         * {@code UnsupportedOperationException}.
060         * 
061         * @return True when this {@code WebResponse} supports setting headers.
062         */
063        public abstract boolean isHeaderSupported();
064        
065        /**
066         * Set a header to the string value in the servlet response stream.
067         * 
068         * @param name
069         * @param value
070         */
071        public abstract void setHeader(String name, String value);
072
073        /**
074         * Add a value to the servlet response stream.
075         * 
076         * @param name
077         * @param value
078         */
079        public abstract void addHeader(String name, String value);
080
081        /**
082         * Set a header to the date value in the servlet response stream.
083         * 
084         * @param name
085         * @param date
086         */
087        public abstract void setDateHeader(String name, Instant date);
088
089        /**
090         * Set the content length on the response, if appropriate in the subclass. This default
091         * implementation does nothing.
092         * 
093         * @param length
094         *            The length of the content
095         */
096        public abstract void setContentLength(final long length);
097
098        /**
099         * Set the content type on the response, if appropriate in the subclass. This default
100         * implementation does nothing.
101         * 
102         * @param mimeType
103         *            The mime type
104         */
105        public abstract void setContentType(final String mimeType);
106
107        /**
108         * Sets the content range of the response. If no content range is set the client assumes the
109         * whole content. Please note that if the content range is set, the content length, the status
110         * code and the accept range must be set right, too.
111         *
112         * @param contentRange
113         *            the content range
114         */
115        public void setContentRange(final String contentRange)
116        {
117                setHeader("Content-Range", contentRange);
118        }
119
120
121        /**
122         * Sets the accept range (e.g. bytes)
123         *
124         * @param acceptRange
125         *            the accept range header information
126         */
127        public void setAcceptRange(final String acceptRange)
128        {
129                setHeader("Accept-Range", acceptRange);
130
131        }
132
133        /**
134         * Set the contents last modified time, if appropriate in the subclass.
135         * 
136         * @param time
137         *            The last modified time
138         */
139        public void setLastModifiedTime(final Instant time)
140        {
141                setDateHeader("Last-Modified", time);
142        }
143
144        /**
145         * Convenience method for setting the content-disposition:attachment header. This header is used
146         * if the response should prompt the user to download it as a file instead of opening in a
147         * browser.
148         * <p>
149         * The file name will be <a href="http://greenbytes.de/tech/tc2231/">encoded</a>
150         *
151         * @param filename
152         *            file name of the attachment
153         */
154        public void setAttachmentHeader(final String filename)
155        {
156                setHeader("Content-Disposition", "attachment" + encodeDispositionHeaderValue(filename));
157        }
158
159        /**
160         * Convenience method for setting the content-disposition:inline header. This header is used if
161         * the response should be shown embedded in browser window while having custom file name when
162         * user saves the response. browser.
163         * <p>
164         * The file name will be <a href="http://greenbytes.de/tech/tc2231/">encoded</a>
165         *
166         * @param filename
167         *            file name of the attachment
168         */
169        public void setInlineHeader(final String filename)
170        {
171                setHeader("Content-Disposition", "inline" + encodeDispositionHeaderValue(filename));
172        }
173
174        /**
175         * <a href="http://greenbytes.de/tech/tc2231/">Encodes</a> the value of the filename used in
176         * "Content-Disposition" response header
177         *
178         * @param filename
179         *            the non-encoded file name
180         * @return encoded filename
181         */
182        private String encodeDispositionHeaderValue(final String filename)
183        {
184                return (Strings.isEmpty(filename) ? "" : String.format(
185                        "; filename=\"%1$s\"; filename*=UTF-8''%1$s",
186                        UrlEncoder.HEADER_INSTANCE.encode(filename, StandardCharsets.UTF_8)));
187        }
188
189        /**
190         * Sets the status code for this response.
191         * 
192         * @param sc
193         *            status code
194         */
195        public abstract void setStatus(int sc);
196
197        /**
198         * Send error status code with optional message.
199         * 
200         * @param sc
201         * @param msg
202         */
203        public abstract void sendError(int sc, String msg);
204
205        /**
206         * Encodes urls used to redirect. Sometimes rules for encoding URLs for redirecting differ from
207         * encoding URLs for links, so this method is broken out away form
208         * {@link #encodeURL(CharSequence)}.
209         * 
210         * @param url
211         * @return encoded URL
212         */
213        public abstract String encodeRedirectURL(CharSequence url);
214
215        /**
216         * Redirects the response to specified URL. The implementation is responsible for properly
217         * encoding the URL. Implementations of this method should run passed in {@code url} parameters
218         * through the {@link #encodeRedirectURL(CharSequence)} method.
219         * 
220         * @param url
221         */
222        public abstract void sendRedirect(String url);
223
224        /**
225         * @return <code>true</code> is {@link #sendRedirect(String)} was called, <code>false</code>
226         *         otherwise.
227         */
228        public abstract boolean isRedirect();
229
230        /**
231         * Flushes the response.
232         */
233        public abstract void flush();
234
235        /**
236         * Make this response non-cacheable
237         */
238        public void disableCaching()
239        {
240                setDateHeader("Date", Instant.now());
241                setDateHeader("Expires", Instant.EPOCH);
242                setHeader("Pragma", "no-cache");
243                setHeader("Cache-Control", "no-cache, no-store");
244        }
245
246        /**
247         * Make this response cacheable
248         * <p/>
249         * when trying to enable caching for web pages check this out: <a
250         * href="https://issues.apache.org/jira/browse/WICKET-4357">WICKET-4357</a>
251         * 
252         * @param duration
253         *            maximum duration before the response must be invalidated by any caches. It should
254         *            not exceed one year, based on <a
255         *            href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html">RFC-2616</a>.
256         * @param scope
257         *            controls which caches are allowed to cache the response
258         *
259         * @see WebResponse#MAX_CACHE_DURATION
260         */
261        public void enableCaching(Duration duration, final WebResponse.CacheScope scope)
262        {
263                Args.notNull(duration, "duration");
264                Args.notNull(scope, "scope");
265
266                // do not exceed the maximum recommended value from RFC-2616
267                if (duration.compareTo(MAX_CACHE_DURATION) > 0)
268                {
269                        duration = MAX_CACHE_DURATION;
270                }
271
272                // Get current time
273                Instant now = Instant.now();
274
275                // Time of message generation
276                setDateHeader("Date", now);
277
278                // Time for cache expiry = now + duration
279                setDateHeader("Expires", now.plus(duration));
280
281                // Set cache scope
282                setHeader("Cache-Control", scope.cacheControl);
283
284                // Set maximum age for caching in seconds (rounded)
285                addHeader("Cache-Control", "max-age=" + Math.round(duration.getSeconds()));
286
287                // Though 'cache' is not an official value it will eliminate an eventual 'no-cache' header
288                setHeader("Pragma", "cache");
289        }
290
291        /**
292         * caching scope for data
293         * <p/>
294         * Unless the data is confidential, session-specific or user-specific the general advice is to
295         * prefer value <code>PUBLIC</code> for best network performance.
296         * <p/>
297         * This value will basically affect the header [Cache-Control]. Details can be found <a
298         * href="http://palisade.plynt.com/issues/2008Jul/cache-control-attributes">here</a> or in <a
299         * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html">RFC-2616</a>.
300         */
301        public static enum CacheScope
302        {
303                /**
304                 * use all caches (private + public)
305                 * <p/>
306                 * Use this value for caching if the data is not confidential or session-specific. It will
307                 * allow public caches to cache the data. In some versions of Firefox this will enable
308                 * caching of resources over SSL (details can be found <a
309                 * href="http://blog.pluron.com/2008/07/why-you-should.html">here</a>).
310                 */
311                PUBLIC("public"),
312                /**
313                 * only use non-public caches
314                 * <p/>
315                 * Use this setting if the response is session-specific or confidential and you don't want
316                 * it to be cached on public caches or proxies. On some versions of Firefox this will
317                 * disable caching of any resources in over SSL connections.
318                 */
319                PRIVATE("private");
320
321                // value for Cache-Control header
322                private final String cacheControl;
323
324                CacheScope(final String cacheControl)
325                {
326                        this.cacheControl = cacheControl;
327                }
328        }
329}