001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.http;
018
019import java.util.concurrent.TimeUnit;
020
021import org.springframework.util.StringUtils;
022
023/**
024 * A builder for creating "Cache-Control" HTTP response headers.
025 *
026 * <p>Adding Cache-Control directives to HTTP responses can significantly improve the client
027 * experience when interacting with a web application. This builder creates opinionated
028 * "Cache-Control" headers with response directives only, with several use cases in mind.
029 *
030 * <ul>
031 * <li>Caching HTTP responses with {@code CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS)}
032 * will result in {@code Cache-Control: "max-age=3600"}</li>
033 * <li>Preventing cache with {@code CacheControl cc = CacheControl.noStore()}
034 * will result in {@code Cache-Control: "no-store"}</li>
035 * <li>Advanced cases like {@code CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS).noTransform().cachePublic()}
036 * will result in {@code Cache-Control: "max-age=3600, no-transform, public"}</li>
037 * </ul>
038 *
039 * <p>Note that to be efficient, Cache-Control headers should be written along HTTP validators
040 * such as "Last-Modified" or "ETag" headers.
041 *
042 * @author Brian Clozel
043 * @author Juergen Hoeller
044 * @since 4.2
045 * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2">rfc7234 section 5.2.2</a>
046 * @see <a href="https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching">
047 * HTTP caching - Google developers reference</a>
048 * @see <a href="https://www.mnot.net/cache_docs/">Mark Nottingham's cache documentation</a>
049 */
050public class CacheControl {
051
052        private long maxAge = -1;
053
054        private boolean noCache = false;
055
056        private boolean noStore = false;
057
058        private boolean mustRevalidate = false;
059
060        private boolean noTransform = false;
061
062        private boolean cachePublic = false;
063
064        private boolean cachePrivate = false;
065
066        private boolean proxyRevalidate = false;
067
068        private long staleWhileRevalidate = -1;
069
070        private long staleIfError = -1;
071
072        private long sMaxAge = -1;
073
074
075        /**
076         * Create an empty CacheControl instance.
077         * @see #empty()
078         */
079        protected CacheControl() {
080        }
081
082
083        /**
084         * Return an empty directive.
085         * <p>This is well suited for using other optional directives without "max-age",
086         * "no-cache" or "no-store".
087         * @return {@code this}, to facilitate method chaining
088         */
089        public static CacheControl empty() {
090                return new CacheControl();
091        }
092
093        /**
094         * Add a "max-age=" directive.
095         * <p>This directive is well suited for publicly caching resources, knowing that
096         * they won't change within the configured amount of time. Additional directives
097         * can be also used, in case resources shouldn't be cached ({@link #cachePrivate()})
098         * or transformed ({@link #noTransform()}) by shared caches.
099         * <p>In order to prevent caches to reuse the cached response even when it has
100         * become stale (i.e. the "max-age" delay is passed), the "must-revalidate"
101         * directive should be set ({@link #mustRevalidate()}
102         * @param maxAge the maximum time the response should be cached
103         * @param unit the time unit of the {@code maxAge} argument
104         * @return {@code this}, to facilitate method chaining
105         * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.8">rfc7234 section 5.2.2.8</a>
106         */
107        public static CacheControl maxAge(long maxAge, TimeUnit unit) {
108                CacheControl cc = new CacheControl();
109                cc.maxAge = unit.toSeconds(maxAge);
110                return cc;
111        }
112
113        /**
114         * Add a "no-cache" directive.
115         * <p>This directive is well suited for telling caches that the response
116         * can be reused only if the client revalidates it with the server.
117         * This directive won't disable cache altogether and may result with clients
118         * sending conditional requests (with "ETag", "If-Modified-Since" headers)
119         * and the server responding with "304 - Not Modified" status.
120         * <p>In order to disable caching and minimize requests/responses exchanges,
121         * the {@link #noStore()} directive should be used instead of {@code #noCache()}.
122         * @return {@code this}, to facilitate method chaining
123         * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.2">rfc7234 section 5.2.2.2</a>
124         */
125        public static CacheControl noCache() {
126                CacheControl cc = new CacheControl();
127                cc.noCache = true;
128                return cc;
129        }
130
131        /**
132         * Add a "no-store" directive.
133         * <p>This directive is well suited for preventing caches (browsers and proxies)
134         * to cache the content of responses.
135         * @return {@code this}, to facilitate method chaining
136         * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.3">rfc7234 section 5.2.2.3</a>
137         */
138        public static CacheControl noStore() {
139                CacheControl cc = new CacheControl();
140                cc.noStore = true;
141                return cc;
142        }
143
144
145        /**
146         * Add a "must-revalidate" directive.
147         * <p>This directive indicates that once it has become stale, a cache MUST NOT
148         * use the response to satisfy subsequent requests without successful validation
149         * on the origin server.
150         * @return {@code this}, to facilitate method chaining
151         * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.1">rfc7234 section 5.2.2.1</a>
152         */
153        public CacheControl mustRevalidate() {
154                this.mustRevalidate = true;
155                return this;
156        }
157
158        /**
159         * Add a "no-transform" directive.
160         * <p>This directive indicates that intermediaries (caches and others) should
161         * not transform the response content. This can be useful to force caches and
162         * CDNs not to automatically gzip or optimize the response content.
163         * @return {@code this}, to facilitate method chaining
164         * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.4">rfc7234 section 5.2.2.4</a>
165         */
166        public CacheControl noTransform() {
167                this.noTransform = true;
168                return this;
169        }
170
171        /**
172         * Add a "public" directive.
173         * <p>This directive indicates that any cache MAY store the response,
174         * even if the response would normally be non-cacheable or cacheable
175         * only within a private cache.
176         * @return {@code this}, to facilitate method chaining
177         * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.5">rfc7234 section 5.2.2.5</a>
178         */
179        public CacheControl cachePublic() {
180                this.cachePublic = true;
181                return this;
182        }
183
184        /**
185         * Add a "private" directive.
186         * <p>This directive indicates that the response message is intended
187         * for a single user and MUST NOT be stored by a shared cache.
188         * @return {@code this}, to facilitate method chaining
189         * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.6">rfc7234 section 5.2.2.6</a>
190         */
191        public CacheControl cachePrivate() {
192                this.cachePrivate = true;
193                return this;
194        }
195
196        /**
197         * Add a "proxy-revalidate" directive.
198         * <p>This directive has the same meaning as the "must-revalidate" directive,
199         * except that it does not apply to private caches (i.e. browsers, HTTP clients).
200         * @return {@code this}, to facilitate method chaining
201         * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.7">rfc7234 section 5.2.2.7</a>
202         */
203        public CacheControl proxyRevalidate() {
204                this.proxyRevalidate = true;
205                return this;
206        }
207
208        /**
209         * Add an "s-maxage" directive.
210         * <p>This directive indicates that, in shared caches, the maximum age specified
211         * by this directive overrides the maximum age specified by other directives.
212         * @param sMaxAge the maximum time the response should be cached
213         * @param unit the time unit of the {@code sMaxAge} argument
214         * @return {@code this}, to facilitate method chaining
215         * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.9">rfc7234 section 5.2.2.9</a>
216         */
217        public CacheControl sMaxAge(long sMaxAge, TimeUnit unit) {
218                this.sMaxAge = unit.toSeconds(sMaxAge);
219                return this;
220        }
221
222        /**
223         * Add a "stale-while-revalidate" directive.
224         * <p>This directive indicates that caches MAY serve the response in which it
225         * appears after it becomes stale, up to the indicated number of seconds.
226         * If a cached response is served stale due to the presence of this extension,
227         * the cache SHOULD attempt to revalidate it while still serving stale responses
228         * (i.e. without blocking).
229         * @param staleWhileRevalidate the maximum time the response should be used while being revalidated
230         * @param unit the time unit of the {@code staleWhileRevalidate} argument
231         * @return {@code this}, to facilitate method chaining
232         * @see <a href="https://tools.ietf.org/html/rfc5861#section-3">rfc5861 section 3</a>
233         */
234        public CacheControl staleWhileRevalidate(long staleWhileRevalidate, TimeUnit unit) {
235                this.staleWhileRevalidate = unit.toSeconds(staleWhileRevalidate);
236                return this;
237        }
238
239        /**
240         * Add a "stale-if-error" directive.
241         * <p>This directive indicates that when an error is encountered, a cached stale response
242         * MAY be used to satisfy the request, regardless of other freshness information.
243         * @param staleIfError the maximum time the response should be used when errors are encountered
244         * @param unit the time unit of the {@code staleIfError} argument
245         * @return {@code this}, to facilitate method chaining
246         * @see <a href="https://tools.ietf.org/html/rfc5861#section-4">rfc5861 section 4</a>
247         */
248        public CacheControl staleIfError(long staleIfError, TimeUnit unit) {
249                this.staleIfError = unit.toSeconds(staleIfError);
250                return this;
251        }
252
253
254        /**
255         * Return the "Cache-Control" header value, if any.
256         * @return the header value, or {@code null} if no directive was added
257         */
258        public String getHeaderValue() {
259                StringBuilder headerValue = new StringBuilder();
260                if (this.maxAge != -1) {
261                        appendDirective(headerValue, "max-age=" + this.maxAge);
262                }
263                if (this.noCache) {
264                        appendDirective(headerValue, "no-cache");
265                }
266                if (this.noStore) {
267                        appendDirective(headerValue, "no-store");
268                }
269                if (this.mustRevalidate) {
270                        appendDirective(headerValue, "must-revalidate");
271                }
272                if (this.noTransform) {
273                        appendDirective(headerValue, "no-transform");
274                }
275                if (this.cachePublic) {
276                        appendDirective(headerValue, "public");
277                }
278                if (this.cachePrivate) {
279                        appendDirective(headerValue, "private");
280                }
281                if (this.proxyRevalidate) {
282                        appendDirective(headerValue, "proxy-revalidate");
283                }
284                if (this.sMaxAge != -1) {
285                        appendDirective(headerValue, "s-maxage=" + this.sMaxAge);
286                }
287                if (this.staleIfError != -1) {
288                        appendDirective(headerValue, "stale-if-error=" + this.staleIfError);
289                }
290                if (this.staleWhileRevalidate != -1) {
291                        appendDirective(headerValue, "stale-while-revalidate=" + this.staleWhileRevalidate);
292                }
293
294                String valueString = headerValue.toString();
295                return (StringUtils.hasText(valueString) ? valueString : null);
296        }
297
298        private void appendDirective(StringBuilder builder, String value) {
299                if (builder.length() > 0) {
300                        builder.append(", ");
301                }
302                builder.append(value);
303        }
304
305}