001/*
002 * Copyright 2012-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 *      http://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.boot.autoconfigure.web;
018
019import java.time.Duration;
020import java.time.temporal.ChronoUnit;
021import java.util.concurrent.TimeUnit;
022
023import org.springframework.boot.context.properties.ConfigurationProperties;
024import org.springframework.boot.context.properties.PropertyMapper;
025import org.springframework.boot.convert.DurationUnit;
026import org.springframework.http.CacheControl;
027
028/**
029 * Properties used to configure resource handling.
030 *
031 * @author Phillip Webb
032 * @author Brian Clozel
033 * @author Dave Syer
034 * @author Venil Noronha
035 * @author Kristine Jetzke
036 * @since 1.1.0
037 */
038@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
039public class ResourceProperties {
040
041        private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
042                        "classpath:/META-INF/resources/", "classpath:/resources/",
043                        "classpath:/static/", "classpath:/public/" };
044
045        /**
046         * Locations of static resources. Defaults to classpath:[/META-INF/resources/,
047         * /resources/, /static/, /public/].
048         */
049        private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
050
051        /**
052         * Whether to enable default resource handling.
053         */
054        private boolean addMappings = true;
055
056        private final Chain chain = new Chain();
057
058        private final Cache cache = new Cache();
059
060        public String[] getStaticLocations() {
061                return this.staticLocations;
062        }
063
064        public void setStaticLocations(String[] staticLocations) {
065                this.staticLocations = appendSlashIfNecessary(staticLocations);
066        }
067
068        private String[] appendSlashIfNecessary(String[] staticLocations) {
069                String[] normalized = new String[staticLocations.length];
070                for (int i = 0; i < staticLocations.length; i++) {
071                        String location = staticLocations[i];
072                        normalized[i] = location.endsWith("/") ? location : location + "/";
073                }
074                return normalized;
075        }
076
077        public boolean isAddMappings() {
078                return this.addMappings;
079        }
080
081        public void setAddMappings(boolean addMappings) {
082                this.addMappings = addMappings;
083        }
084
085        public Chain getChain() {
086                return this.chain;
087        }
088
089        public Cache getCache() {
090                return this.cache;
091        }
092
093        /**
094         * Configuration for the Spring Resource Handling chain.
095         */
096        public static class Chain {
097
098                /**
099                 * Whether to enable the Spring Resource Handling chain. By default, disabled
100                 * unless at least one strategy has been enabled.
101                 */
102                private Boolean enabled;
103
104                /**
105                 * Whether to enable caching in the Resource chain.
106                 */
107                private boolean cache = true;
108
109                /**
110                 * Whether to enable HTML5 application cache manifest rewriting.
111                 */
112                private boolean htmlApplicationCache = false;
113
114                /**
115                 * Whether to enable resolution of already compressed resources (gzip, brotli).
116                 * Checks for a resource name with the '.gz' or '.br' file extensions.
117                 */
118                private boolean compressed = false;
119
120                private final Strategy strategy = new Strategy();
121
122                /**
123                 * Return whether the resource chain is enabled. Return {@code null} if no
124                 * specific settings are present.
125                 * @return whether the resource chain is enabled or {@code null} if no specified
126                 * settings are present.
127                 */
128                public Boolean getEnabled() {
129                        return getEnabled(getStrategy().getFixed().isEnabled(),
130                                        getStrategy().getContent().isEnabled(), this.enabled);
131                }
132
133                public void setEnabled(boolean enabled) {
134                        this.enabled = enabled;
135                }
136
137                public boolean isCache() {
138                        return this.cache;
139                }
140
141                public void setCache(boolean cache) {
142                        this.cache = cache;
143                }
144
145                public Strategy getStrategy() {
146                        return this.strategy;
147                }
148
149                public boolean isHtmlApplicationCache() {
150                        return this.htmlApplicationCache;
151                }
152
153                public void setHtmlApplicationCache(boolean htmlApplicationCache) {
154                        this.htmlApplicationCache = htmlApplicationCache;
155                }
156
157                public boolean isCompressed() {
158                        return this.compressed;
159                }
160
161                public void setCompressed(boolean compressed) {
162                        this.compressed = compressed;
163                }
164
165                static Boolean getEnabled(boolean fixedEnabled, boolean contentEnabled,
166                                Boolean chainEnabled) {
167                        return (fixedEnabled || contentEnabled) ? Boolean.TRUE : chainEnabled;
168                }
169
170        }
171
172        /**
173         * Strategies for extracting and embedding a resource version in its URL path.
174         */
175        public static class Strategy {
176
177                private final Fixed fixed = new Fixed();
178
179                private final Content content = new Content();
180
181                public Fixed getFixed() {
182                        return this.fixed;
183                }
184
185                public Content getContent() {
186                        return this.content;
187                }
188
189        }
190
191        /**
192         * Version Strategy based on content hashing.
193         */
194        public static class Content {
195
196                /**
197                 * Whether to enable the content Version Strategy.
198                 */
199                private boolean enabled;
200
201                /**
202                 * Comma-separated list of patterns to apply to the content Version Strategy.
203                 */
204                private String[] paths = new String[] { "/**" };
205
206                public boolean isEnabled() {
207                        return this.enabled;
208                }
209
210                public void setEnabled(boolean enabled) {
211                        this.enabled = enabled;
212                }
213
214                public String[] getPaths() {
215                        return this.paths;
216                }
217
218                public void setPaths(String[] paths) {
219                        this.paths = paths;
220                }
221
222        }
223
224        /**
225         * Version Strategy based on a fixed version string.
226         */
227        public static class Fixed {
228
229                /**
230                 * Whether to enable the fixed Version Strategy.
231                 */
232                private boolean enabled;
233
234                /**
235                 * Comma-separated list of patterns to apply to the fixed Version Strategy.
236                 */
237                private String[] paths = new String[] { "/**" };
238
239                /**
240                 * Version string to use for the fixed Version Strategy.
241                 */
242                private String version;
243
244                public boolean isEnabled() {
245                        return this.enabled;
246                }
247
248                public void setEnabled(boolean enabled) {
249                        this.enabled = enabled;
250                }
251
252                public String[] getPaths() {
253                        return this.paths;
254                }
255
256                public void setPaths(String[] paths) {
257                        this.paths = paths;
258                }
259
260                public String getVersion() {
261                        return this.version;
262                }
263
264                public void setVersion(String version) {
265                        this.version = version;
266                }
267
268        }
269
270        /**
271         * Cache configuration.
272         */
273        public static class Cache {
274
275                /**
276                 * Cache period for the resources served by the resource handler. If a duration
277                 * suffix is not specified, seconds will be used. Can be overridden by the
278                 * 'spring.resources.cache.cachecontrol' properties.
279                 */
280                @DurationUnit(ChronoUnit.SECONDS)
281                private Duration period;
282
283                /**
284                 * Cache control HTTP headers, only allows valid directive combinations. Overrides
285                 * the 'spring.resources.cache.period' property.
286                 */
287                private final Cachecontrol cachecontrol = new Cachecontrol();
288
289                public Duration getPeriod() {
290                        return this.period;
291                }
292
293                public void setPeriod(Duration period) {
294                        this.period = period;
295                }
296
297                public Cachecontrol getCachecontrol() {
298                        return this.cachecontrol;
299                }
300
301                /**
302                 * Cache Control HTTP header configuration.
303                 */
304                public static class Cachecontrol {
305
306                        /**
307                         * Maximum time the response should be cached, in seconds if no duration
308                         * suffix is not specified.
309                         */
310                        @DurationUnit(ChronoUnit.SECONDS)
311                        private Duration maxAge;
312
313                        /**
314                         * Indicate that the cached response can be reused only if re-validated with
315                         * the server.
316                         */
317                        private Boolean noCache;
318
319                        /**
320                         * Indicate to not cache the response in any case.
321                         */
322                        private Boolean noStore;
323
324                        /**
325                         * Indicate that once it has become stale, a cache must not use the response
326                         * without re-validating it with the server.
327                         */
328                        private Boolean mustRevalidate;
329
330                        /**
331                         * Indicate intermediaries (caches and others) that they should not transform
332                         * the response content.
333                         */
334                        private Boolean noTransform;
335
336                        /**
337                         * Indicate that any cache may store the response.
338                         */
339                        private Boolean cachePublic;
340
341                        /**
342                         * Indicate that the response message is intended for a single user and must
343                         * not be stored by a shared cache.
344                         */
345                        private Boolean cachePrivate;
346
347                        /**
348                         * Same meaning as the "must-revalidate" directive, except that it does not
349                         * apply to private caches.
350                         */
351                        private Boolean proxyRevalidate;
352
353                        /**
354                         * Maximum time the response can be served after it becomes stale, in seconds
355                         * if no duration suffix is not specified.
356                         */
357                        @DurationUnit(ChronoUnit.SECONDS)
358                        private Duration staleWhileRevalidate;
359
360                        /**
361                         * Maximum time the response may be used when errors are encountered, in
362                         * seconds if no duration suffix is not specified.
363                         */
364                        @DurationUnit(ChronoUnit.SECONDS)
365                        private Duration staleIfError;
366
367                        /**
368                         * Maximum time the response should be cached by shared caches, in seconds if
369                         * no duration suffix is not specified.
370                         */
371                        @DurationUnit(ChronoUnit.SECONDS)
372                        private Duration sMaxAge;
373
374                        public Duration getMaxAge() {
375                                return this.maxAge;
376                        }
377
378                        public void setMaxAge(Duration maxAge) {
379                                this.maxAge = maxAge;
380                        }
381
382                        public Boolean getNoCache() {
383                                return this.noCache;
384                        }
385
386                        public void setNoCache(Boolean noCache) {
387                                this.noCache = noCache;
388                        }
389
390                        public Boolean getNoStore() {
391                                return this.noStore;
392                        }
393
394                        public void setNoStore(Boolean noStore) {
395                                this.noStore = noStore;
396                        }
397
398                        public Boolean getMustRevalidate() {
399                                return this.mustRevalidate;
400                        }
401
402                        public void setMustRevalidate(Boolean mustRevalidate) {
403                                this.mustRevalidate = mustRevalidate;
404                        }
405
406                        public Boolean getNoTransform() {
407                                return this.noTransform;
408                        }
409
410                        public void setNoTransform(Boolean noTransform) {
411                                this.noTransform = noTransform;
412                        }
413
414                        public Boolean getCachePublic() {
415                                return this.cachePublic;
416                        }
417
418                        public void setCachePublic(Boolean cachePublic) {
419                                this.cachePublic = cachePublic;
420                        }
421
422                        public Boolean getCachePrivate() {
423                                return this.cachePrivate;
424                        }
425
426                        public void setCachePrivate(Boolean cachePrivate) {
427                                this.cachePrivate = cachePrivate;
428                        }
429
430                        public Boolean getProxyRevalidate() {
431                                return this.proxyRevalidate;
432                        }
433
434                        public void setProxyRevalidate(Boolean proxyRevalidate) {
435                                this.proxyRevalidate = proxyRevalidate;
436                        }
437
438                        public Duration getStaleWhileRevalidate() {
439                                return this.staleWhileRevalidate;
440                        }
441
442                        public void setStaleWhileRevalidate(Duration staleWhileRevalidate) {
443                                this.staleWhileRevalidate = staleWhileRevalidate;
444                        }
445
446                        public Duration getStaleIfError() {
447                                return this.staleIfError;
448                        }
449
450                        public void setStaleIfError(Duration staleIfError) {
451                                this.staleIfError = staleIfError;
452                        }
453
454                        public Duration getSMaxAge() {
455                                return this.sMaxAge;
456                        }
457
458                        public void setSMaxAge(Duration sMaxAge) {
459                                this.sMaxAge = sMaxAge;
460                        }
461
462                        public CacheControl toHttpCacheControl() {
463                                PropertyMapper map = PropertyMapper.get();
464                                CacheControl control = createCacheControl();
465                                map.from(this::getMustRevalidate).whenTrue()
466                                                .toCall(control::mustRevalidate);
467                                map.from(this::getNoTransform).whenTrue().toCall(control::noTransform);
468                                map.from(this::getCachePublic).whenTrue().toCall(control::cachePublic);
469                                map.from(this::getCachePrivate).whenTrue().toCall(control::cachePrivate);
470                                map.from(this::getProxyRevalidate).whenTrue()
471                                                .toCall(control::proxyRevalidate);
472                                map.from(this::getStaleWhileRevalidate).whenNonNull().to(
473                                                (duration) -> control.staleWhileRevalidate(duration.getSeconds(),
474                                                                TimeUnit.SECONDS));
475                                map.from(this::getStaleIfError).whenNonNull().to((duration) -> control
476                                                .staleIfError(duration.getSeconds(), TimeUnit.SECONDS));
477                                map.from(this::getSMaxAge).whenNonNull().to((duration) -> control
478                                                .sMaxAge(duration.getSeconds(), TimeUnit.SECONDS));
479                                return control;
480                        }
481
482                        private CacheControl createCacheControl() {
483                                if (Boolean.TRUE.equals(this.noStore)) {
484                                        return CacheControl.noStore();
485                                }
486                                if (Boolean.TRUE.equals(this.noCache)) {
487                                        return CacheControl.noCache();
488                                }
489                                if (this.maxAge != null) {
490                                        return CacheControl.maxAge(this.maxAge.getSeconds(),
491                                                        TimeUnit.SECONDS);
492                                }
493                                return CacheControl.empty();
494                        }
495
496                }
497
498        }
499
500}