001/*
002 * Copyright 2002-2019 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.time.Duration;
020import java.util.concurrent.TimeUnit;
021
022import org.springframework.lang.Nullable;
023import org.springframework.util.StringUtils;
024
025/**
026 * A builder for creating "Cache-Control" HTTP response headers.
027 *
028 * <p>Adding Cache-Control directives to HTTP responses can significantly improve the client
029 * experience when interacting with a web application. This builder creates opinionated
030 * "Cache-Control" headers with response directives only, with several use cases in mind.
031 *
032 * <ul>
033 * <li>Caching HTTP responses with {@code CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS)}
034 * will result in {@code Cache-Control: "max-age=3600"}</li>
035 * <li>Preventing cache with {@code CacheControl cc = CacheControl.noStore()}
036 * will result in {@code Cache-Control: "no-store"}</li>
037 * <li>Advanced cases like {@code CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS).noTransform().cachePublic()}
038 * will result in {@code Cache-Control: "max-age=3600, no-transform, public"}</li>
039 * </ul>
040 *
041 * <p>Note that to be efficient, Cache-Control headers should be written along HTTP validators
042 * such as "Last-Modified" or "ETag" headers.
043 *
044 * @author Brian Clozel
045 * @author Juergen Hoeller
046 * @since 4.2
047 * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2">rfc7234 section 5.2.2</a>
048 * @see <a href="https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching">
049 * HTTP caching - Google developers reference</a>
050 * @see <a href="https://www.mnot.net/cache_docs/">Mark Nottingham's cache documentation</a>
051 */
052public class CacheControl {
053
054        @Nullable
055        private Duration maxAge;
056
057        private boolean noCache = false;
058
059        private boolean noStore = false;
060
061        private boolean mustRevalidate = false;
062
063        private boolean noTransform = false;
064
065        private boolean cachePublic = false;
066
067        private boolean cachePrivate = false;
068
069        private boolean proxyRevalidate = false;
070
071        @Nullable
072        private Duration staleWhileRevalidate;
073
074        @Nullable
075        private Duration staleIfError;
076
077        @Nullable
078        private Duration sMaxAge;
079
080
081        /**
082         * Create an empty CacheControl instance.
083         * @see #empty()
084         */
085        protected CacheControl() {
086        }
087
088
089        /**
090         * Return an empty directive.
091         * <p>This is well suited for using other optional directives without "max-age",
092         * "no-cache" or "no-store".
093         * @return {@code this}, to facilitate method chaining
094         */
095        public static CacheControl empty() {
096                return new CacheControl();
097        }
098
099        /**
100         * Add a "max-age=" directive.
101         * <p>This directive is well suited for publicly caching resources, knowing that
102         * they won't change within the configured amount of time. Additional directives
103         * can be also used, in case resources shouldn't be cached ({@link #cachePrivate()})
104         * or transformed ({@link #noTransform()}) by shared caches.
105         * <p>In order to prevent caches to reuse the cached response even when it has
106         * become stale (i.e. the "max-age" delay is passed), the "must-revalidate"
107         * directive should be set ({@link #mustRevalidate()}
108         * @param maxAge the maximum time the response should be cached
109         * @param unit the time unit of the {@code maxAge} argument
110         * @return {@code this}, to facilitate method chaining
111         * @see #maxAge(Duration)
112         * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.8">rfc7234 section 5.2.2.8</a>
113         */
114        public static CacheControl maxAge(long maxAge, TimeUnit unit) {
115                return maxAge(Duration.ofSeconds(unit.toSeconds(maxAge)));
116        }
117
118        /**
119         * Add a "max-age=" directive.
120         * <p>This directive is well suited for publicly caching resources, knowing that
121         * they won't change within the configured amount of time. Additional directives
122         * can be also used, in case resources shouldn't be cached ({@link #cachePrivate()})
123         * or transformed ({@link #noTransform()}) by shared caches.
124         * <p>In order to prevent caches to reuse the cached response even when it has
125         * become stale (i.e. the "max-age" delay is passed), the "must-revalidate"
126         * directive should be set ({@link #mustRevalidate()}
127         * @param maxAge the maximum time the response should be cached
128         * @return {@code this}, to facilitate method chaining
129         * @since 5.2
130         * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.8">rfc7234 section 5.2.2.8</a>
131         */
132        public static CacheControl maxAge(Duration maxAge) {
133                CacheControl cc = new CacheControl();
134                cc.maxAge = maxAge;
135                return cc;
136        }
137
138        /**
139         * Add a "no-cache" directive.
140         * <p>This directive is well suited for telling caches that the response
141         * can be reused only if the client revalidates it with the server.
142         * This directive won't disable cache altogether and may result with clients
143         * sending conditional requests (with "ETag", "If-Modified-Since" headers)
144         * and the server responding with "304 - Not Modified" status.
145         * <p>In order to disable caching and minimize requests/responses exchanges,
146         * the {@link #noStore()} directive should be used instead of {@code #noCache()}.
147         * @return {@code this}, to facilitate method chaining
148         * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.2">rfc7234 section 5.2.2.2</a>
149         */
150        public static CacheControl noCache() {
151                CacheControl cc = new CacheControl();
152                cc.noCache = true;
153                return cc;
154        }
155
156        /**
157         * Add a "no-store" directive.
158         * <p>This directive is well suited for preventing caches (browsers and proxies)
159         * to cache the content of responses.
160         * @return {@code this}, to facilitate method chaining
161         * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.3">rfc7234 section 5.2.2.3</a>
162         */
163        public static CacheControl noStore() {
164                CacheControl cc = new CacheControl();
165                cc.noStore = true;
166                return cc;
167        }
168
169
170        /**
171         * Add a "must-revalidate" directive.
172         * <p>This directive indicates that once it has become stale, a cache MUST NOT
173         * use the response to satisfy subsequent requests without successful validation
174         * on the origin server.
175         * @return {@code this}, to facilitate method chaining
176         * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.1">rfc7234 section 5.2.2.1</a>
177         */
178        public CacheControl mustRevalidate() {
179                this.mustRevalidate = true;
180                return this;
181        }
182
183        /**
184         * Add a "no-transform" directive.
185         * <p>This directive indicates that intermediaries (caches and others) should
186         * not transform the response content. This can be useful to force caches and
187         * CDNs not to automatically gzip or optimize the response content.
188         * @return {@code this}, to facilitate method chaining
189         * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.4">rfc7234 section 5.2.2.4</a>
190         */
191        public CacheControl noTransform() {
192                this.noTransform = true;
193                return this;
194        }
195
196        /**
197         * Add a "public" directive.
198         * <p>This directive indicates that any cache MAY store the response,
199         * even if the response would normally be non-cacheable or cacheable
200         * only within a private cache.
201         * @return {@code this}, to facilitate method chaining
202         * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.5">rfc7234 section 5.2.2.5</a>
203         */
204        public CacheControl cachePublic() {
205                this.cachePublic = true;
206                return this;
207        }
208
209        /**
210         * Add a "private" directive.
211         * <p>This directive indicates that the response message is intended
212         * for a single user and MUST NOT be stored by a shared cache.
213         * @return {@code this}, to facilitate method chaining
214         * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.6">rfc7234 section 5.2.2.6</a>
215         */
216        public CacheControl cachePrivate() {
217                this.cachePrivate = true;
218                return this;
219        }
220
221        /**
222         * Add a "proxy-revalidate" directive.
223         * <p>This directive has the same meaning as the "must-revalidate" directive,
224         * except that it does not apply to private caches (i.e. browsers, HTTP clients).
225         * @return {@code this}, to facilitate method chaining
226         * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.7">rfc7234 section 5.2.2.7</a>
227         */
228        public CacheControl proxyRevalidate() {
229                this.proxyRevalidate = true;
230                return this;
231        }
232
233        /**
234         * Add an "s-maxage" directive.
235         * <p>This directive indicates that, in shared caches, the maximum age specified
236         * by this directive overrides the maximum age specified by other directives.
237         * @param sMaxAge the maximum time the response should be cached
238         * @param unit the time unit of the {@code sMaxAge} argument
239         * @return {@code this}, to facilitate method chaining
240         * @see #sMaxAge(Duration)
241         * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.9">rfc7234 section 5.2.2.9</a>
242         */
243        public CacheControl sMaxAge(long sMaxAge, TimeUnit unit) {
244                return sMaxAge(Duration.ofSeconds(unit.toSeconds(sMaxAge)));
245        }
246
247        /**
248         * Add an "s-maxage" directive.
249         * <p>This directive indicates that, in shared caches, the maximum age specified
250         * by this directive overrides the maximum age specified by other directives.
251         * @param sMaxAge the maximum time the response should be cached
252         * @return {@code this}, to facilitate method chaining
253         * @since 5.2
254         * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.9">rfc7234 section 5.2.2.9</a>
255         */
256        public CacheControl sMaxAge(Duration sMaxAge) {
257                this.sMaxAge = sMaxAge;
258                return this;
259        }
260
261        /**
262         * Add a "stale-while-revalidate" directive.
263         * <p>This directive indicates that caches MAY serve the response in which it
264         * appears after it becomes stale, up to the indicated number of seconds.
265         * If a cached response is served stale due to the presence of this extension,
266         * the cache SHOULD attempt to revalidate it while still serving stale responses
267         * (i.e. without blocking).
268         * @param staleWhileRevalidate the maximum time the response should be used while being revalidated
269         * @param unit the time unit of the {@code staleWhileRevalidate} argument
270         * @return {@code this}, to facilitate method chaining
271         * @see #staleWhileRevalidate(Duration)
272         * @see <a href="https://tools.ietf.org/html/rfc5861#section-3">rfc5861 section 3</a>
273         */
274        public CacheControl staleWhileRevalidate(long staleWhileRevalidate, TimeUnit unit) {
275                return staleWhileRevalidate(Duration.ofSeconds(unit.toSeconds(staleWhileRevalidate)));
276        }
277
278        /**
279         * Add a "stale-while-revalidate" directive.
280         * <p>This directive indicates that caches MAY serve the response in which it
281         * appears after it becomes stale, up to the indicated number of seconds.
282         * If a cached response is served stale due to the presence of this extension,
283         * the cache SHOULD attempt to revalidate it while still serving stale responses
284         * (i.e. without blocking).
285         * @param staleWhileRevalidate the maximum time the response should be used while being revalidated
286         * @return {@code this}, to facilitate method chaining
287         * @since 5.2
288         * @see <a href="https://tools.ietf.org/html/rfc5861#section-3">rfc5861 section 3</a>
289         */
290        public CacheControl staleWhileRevalidate(Duration staleWhileRevalidate) {
291                this.staleWhileRevalidate = staleWhileRevalidate;
292                return this;
293        }
294
295        /**
296         * Add a "stale-if-error" directive.
297         * <p>This directive indicates that when an error is encountered, a cached stale response
298         * MAY be used to satisfy the request, regardless of other freshness information.
299         * @param staleIfError the maximum time the response should be used when errors are encountered
300         * @param unit the time unit of the {@code staleIfError} argument
301         * @return {@code this}, to facilitate method chaining
302         * @see #staleIfError(Duration)
303         * @see <a href="https://tools.ietf.org/html/rfc5861#section-4">rfc5861 section 4</a>
304         */
305        public CacheControl staleIfError(long staleIfError, TimeUnit unit) {
306                return staleIfError(Duration.ofSeconds(unit.toSeconds(staleIfError)));
307        }
308
309        /**
310         * Add a "stale-if-error" directive.
311         * <p>This directive indicates that when an error is encountered, a cached stale response
312         * MAY be used to satisfy the request, regardless of other freshness information.
313         * @param staleIfError the maximum time the response should be used when errors are encountered
314         * @return {@code this}, to facilitate method chaining
315         * @since 5.2
316         * @see <a href="https://tools.ietf.org/html/rfc5861#section-4">rfc5861 section 4</a>
317         */
318        public CacheControl staleIfError(Duration staleIfError) {
319                this.staleIfError = staleIfError;
320                return this;
321        }
322
323        /**
324         * Return the "Cache-Control" header value, if any.
325         * @return the header value, or {@code null} if no directive was added
326         */
327        @Nullable
328        public String getHeaderValue() {
329                String headerValue = toHeaderValue();
330                return (StringUtils.hasText(headerValue) ? headerValue : null);
331        }
332
333        /**
334         * Return the "Cache-Control" header value.
335         * @return the header value (potentially empty)
336         */
337        private String toHeaderValue() {
338                StringBuilder headerValue = new StringBuilder();
339                if (this.maxAge != null) {
340                        appendDirective(headerValue, "max-age=" + this.maxAge.getSeconds());
341                }
342                if (this.noCache) {
343                        appendDirective(headerValue, "no-cache");
344                }
345                if (this.noStore) {
346                        appendDirective(headerValue, "no-store");
347                }
348                if (this.mustRevalidate) {
349                        appendDirective(headerValue, "must-revalidate");
350                }
351                if (this.noTransform) {
352                        appendDirective(headerValue, "no-transform");
353                }
354                if (this.cachePublic) {
355                        appendDirective(headerValue, "public");
356                }
357                if (this.cachePrivate) {
358                        appendDirective(headerValue, "private");
359                }
360                if (this.proxyRevalidate) {
361                        appendDirective(headerValue, "proxy-revalidate");
362                }
363                if (this.sMaxAge != null) {
364                        appendDirective(headerValue, "s-maxage=" + this.sMaxAge.getSeconds());
365                }
366                if (this.staleIfError != null) {
367                        appendDirective(headerValue, "stale-if-error=" + this.staleIfError.getSeconds());
368                }
369                if (this.staleWhileRevalidate != null) {
370                        appendDirective(headerValue, "stale-while-revalidate=" + this.staleWhileRevalidate.getSeconds());
371                }
372                return headerValue.toString();
373        }
374
375        private void appendDirective(StringBuilder builder, String value) {
376                if (builder.length() > 0) {
377                        builder.append(", ");
378                }
379                builder.append(value);
380        }
381
382
383        @Override
384        public String toString() {
385                return "CacheControl [" + toHeaderValue() + "]";
386        }
387
388}