001/*
002 * Copyright 2002-2020 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.web.accept;
018
019import java.util.ArrayList;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Locale;
023import java.util.Map;
024import java.util.Properties;
025
026import javax.servlet.ServletContext;
027
028import org.springframework.beans.factory.FactoryBean;
029import org.springframework.beans.factory.InitializingBean;
030import org.springframework.http.MediaType;
031import org.springframework.http.MediaTypeFactory;
032import org.springframework.lang.Nullable;
033import org.springframework.util.Assert;
034import org.springframework.util.CollectionUtils;
035import org.springframework.web.context.ServletContextAware;
036
037/**
038 * Factory to create a {@code ContentNegotiationManager} and configure it with
039 * {@link ContentNegotiationStrategy} instances.
040 *
041 * <p>This factory offers properties that in turn result in configuring the
042 * underlying strategies. The table below shows the property names, their
043 * default settings, as well as the strategies that they help to configure:
044 *
045 * <table>
046 * <tr>
047 * <th>Property Setter</th>
048 * <th>Default Value</th>
049 * <th>Underlying Strategy</th>
050 * <th>Enabled Or Not</th>
051 * </tr>
052 * <tr>
053 * <td>{@link #setFavorPathExtension favorPathExtension}</td>
054 * <td>true</td>
055 * <td>{@link PathExtensionContentNegotiationStrategy}</td>
056 * <td>Enabled</td>
057 * </tr>
058 * <tr>
059 * <td>{@link #setFavorParameter favorParameter}</td>
060 * <td>false</td>
061 * <td>{@link ParameterContentNegotiationStrategy}</td>
062 * <td>Off</td>
063 * </tr>
064 * <tr>
065 * <td>{@link #setIgnoreAcceptHeader ignoreAcceptHeader}</td>
066 * <td>false</td>
067 * <td>{@link HeaderContentNegotiationStrategy}</td>
068 * <td>Enabled</td>
069 * </tr>
070 * <tr>
071 * <td>{@link #setDefaultContentType defaultContentType}</td>
072 * <td>null</td>
073 * <td>{@link FixedContentNegotiationStrategy}</td>
074 * <td>Off</td>
075 * </tr>
076 * <tr>
077 * <td>{@link #setDefaultContentTypeStrategy defaultContentTypeStrategy}</td>
078 * <td>null</td>
079 * <td>{@link ContentNegotiationStrategy}</td>
080 * <td>Off</td>
081 * </tr>
082 * </table>
083 *
084 * <p>Alternatively you can avoid use of the above convenience builder
085 * methods and set the exact strategies to use via
086 * {@link #setStrategies(List)}.
087 *
088 * <p><strong>Deprecation Note:</strong> As of 5.2.4,
089 * {@link #setFavorPathExtension(boolean) favorPathExtension} and
090 * {@link #setIgnoreUnknownPathExtensions(boolean) ignoreUnknownPathExtensions}
091 * are deprecated in order to discourage using path extensions for content
092 * negotiation and for request mapping with similar deprecations on
093 * {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
094 * RequestMappingHandlerMapping}. For further context, please read issue
095 * <a href="https://github.com/spring-projects/spring-framework/issues/24179">#24719</a>.
096 * @author Rossen Stoyanchev
097 * @author Brian Clozel
098 * @since 3.2
099 */
100public class ContentNegotiationManagerFactoryBean
101                implements FactoryBean<ContentNegotiationManager>, ServletContextAware, InitializingBean {
102
103        @Nullable
104        private List<ContentNegotiationStrategy> strategies;
105
106
107        private boolean favorPathExtension = true;
108
109        private boolean favorParameter = false;
110
111        private boolean ignoreAcceptHeader = false;
112
113        private Map<String, MediaType> mediaTypes = new HashMap<>();
114
115        private boolean ignoreUnknownPathExtensions = true;
116
117        @Nullable
118        private Boolean useRegisteredExtensionsOnly;
119
120        private String parameterName = "format";
121
122        @Nullable
123        private ContentNegotiationStrategy defaultNegotiationStrategy;
124
125        @Nullable
126        private ContentNegotiationManager contentNegotiationManager;
127
128        @Nullable
129        private ServletContext servletContext;
130
131
132        /**
133         * Set the exact list of strategies to use.
134         * <p><strong>Note:</strong> use of this method is mutually exclusive with
135         * use of all other setters in this class which customize a default, fixed
136         * set of strategies. See class level doc for more details.
137         * @param strategies the strategies to use
138         * @since 5.0
139         */
140        public void setStrategies(@Nullable List<ContentNegotiationStrategy> strategies) {
141                this.strategies = (strategies != null ? new ArrayList<>(strategies) : null);
142        }
143
144        /**
145         * Whether the path extension in the URL path should be used to determine
146         * the requested media type.
147         * <p>By default this is set to {@code true} in which case a request
148         * for {@code /hotels.pdf} will be interpreted as a request for
149         * {@code "application/pdf"} regardless of the 'Accept' header.
150         * @deprecated as of 5.2.4. See class-level note on the deprecation of path
151         * extension config options. As there is no replacement for this method,
152         * for the time being it's necessary to continue using it in order to set it
153         * to {@code false}. In 5.3 when {@code false} becomes the default, use of
154         * this property will no longer be necessary.
155         */
156        @Deprecated
157        public void setFavorPathExtension(boolean favorPathExtension) {
158                this.favorPathExtension = favorPathExtension;
159        }
160
161        /**
162         * Add a mapping from a key to a MediaType where the key are normalized to
163         * lowercase and may have been extracted from a path extension, a filename
164         * extension, or passed as a query parameter.
165         * <p>The {@link #setFavorParameter(boolean) parameter strategy} requires
166         * such mappings in order to work while the {@link #setFavorPathExtension(boolean)
167         * path extension strategy} can fall back on lookups via
168         * {@link ServletContext#getMimeType} and
169         * {@link org.springframework.http.MediaTypeFactory}.
170         * <p><strong>Note:</strong> Mappings registered here may be accessed via
171         * {@link ContentNegotiationManager#getMediaTypeMappings()} and may be used
172         * not only in the parameter and path extension strategies. For example,
173         * with the Spring MVC config, e.g. {@code @EnableWebMvc} or
174         * {@code <mvc:annotation-driven>}, the media type mappings are also plugged
175         * in to:
176         * <ul>
177         * <li>Determine the media type of static resources served with
178         * {@code ResourceHttpRequestHandler}.
179         * <li>Determine the media type of views rendered with
180         * {@code ContentNegotiatingViewResolver}.
181         * <li>List safe extensions for RFD attack detection (check the Spring
182         * Framework reference docs for details).
183         * </ul>
184         * @param mediaTypes media type mappings
185         * @see #addMediaType(String, MediaType)
186         * @see #addMediaTypes(Map)
187         */
188        public void setMediaTypes(Properties mediaTypes) {
189                if (!CollectionUtils.isEmpty(mediaTypes)) {
190                        mediaTypes.forEach((key, value) ->
191                                        addMediaType((String) key, MediaType.valueOf((String) value)));
192                }
193        }
194
195        /**
196         * An alternative to {@link #setMediaTypes} for programmatic registrations.
197         */
198        public void addMediaType(String key, MediaType mediaType) {
199                this.mediaTypes.put(key.toLowerCase(Locale.ENGLISH), mediaType);
200        }
201
202        /**
203         * An alternative to {@link #setMediaTypes} for programmatic registrations.
204         */
205        public void addMediaTypes(@Nullable Map<String, MediaType> mediaTypes) {
206                if (mediaTypes != null) {
207                        mediaTypes.forEach(this::addMediaType);
208                }
209        }
210
211        /**
212         * Whether to ignore requests with path extension that cannot be resolved
213         * to any media type. Setting this to {@code false} will result in an
214         * {@code HttpMediaTypeNotAcceptableException} if there is no match.
215         * <p>By default this is set to {@code true}.
216         * @deprecated as of 5.2.4. See class-level note on the deprecation of path
217         * extension config options.
218         */
219        @Deprecated
220        public void setIgnoreUnknownPathExtensions(boolean ignore) {
221                this.ignoreUnknownPathExtensions = ignore;
222        }
223
224        /**
225         * Indicate whether to use the Java Activation Framework as a fallback option
226         * to map from file extensions to media types.
227         * @deprecated as of 5.0, in favor of {@link #setUseRegisteredExtensionsOnly(boolean)}, which
228         * has reverse behavior.
229         */
230        @Deprecated
231        public void setUseJaf(boolean useJaf) {
232                setUseRegisteredExtensionsOnly(!useJaf);
233        }
234
235        /**
236         * When {@link #setFavorPathExtension favorPathExtension} or
237         * {@link #setFavorParameter(boolean)} is set, this property determines
238         * whether to use only registered {@code MediaType} mappings or to allow
239         * dynamic resolution, e.g. via {@link MediaTypeFactory}.
240         * <p>By default this is not set in which case dynamic resolution is on.
241         */
242        public void setUseRegisteredExtensionsOnly(boolean useRegisteredExtensionsOnly) {
243                this.useRegisteredExtensionsOnly = useRegisteredExtensionsOnly;
244        }
245
246        private boolean useRegisteredExtensionsOnly() {
247                return (this.useRegisteredExtensionsOnly != null && this.useRegisteredExtensionsOnly);
248        }
249
250        /**
251         * Whether a request parameter ("format" by default) should be used to
252         * determine the requested media type. For this option to work you must
253         * register {@link #setMediaTypes media type mappings}.
254         * <p>By default this is set to {@code false}.
255         * @see #setParameterName
256         */
257        public void setFavorParameter(boolean favorParameter) {
258                this.favorParameter = favorParameter;
259        }
260
261        /**
262         * Set the query parameter name to use when {@link #setFavorParameter} is on.
263         * <p>The default parameter name is {@code "format"}.
264         */
265        public void setParameterName(String parameterName) {
266                Assert.notNull(parameterName, "parameterName is required");
267                this.parameterName = parameterName;
268        }
269
270        /**
271         * Whether to disable checking the 'Accept' request header.
272         * <p>By default this value is set to {@code false}.
273         */
274        public void setIgnoreAcceptHeader(boolean ignoreAcceptHeader) {
275                this.ignoreAcceptHeader = ignoreAcceptHeader;
276        }
277
278        /**
279         * Set the default content type to use when no content type is requested.
280         * <p>By default this is not set.
281         * @see #setDefaultContentTypeStrategy
282         */
283        public void setDefaultContentType(MediaType contentType) {
284                this.defaultNegotiationStrategy = new FixedContentNegotiationStrategy(contentType);
285        }
286
287        /**
288         * Set the default content types to use when no content type is requested.
289         * <p>By default this is not set.
290         * @since 5.0
291         * @see #setDefaultContentTypeStrategy
292         */
293        public void setDefaultContentTypes(List<MediaType> contentTypes) {
294                this.defaultNegotiationStrategy = new FixedContentNegotiationStrategy(contentTypes);
295        }
296
297        /**
298         * Set a custom {@link ContentNegotiationStrategy} to use to determine
299         * the content type to use when no content type is requested.
300         * <p>By default this is not set.
301         * @since 4.1.2
302         * @see #setDefaultContentType
303         */
304        public void setDefaultContentTypeStrategy(ContentNegotiationStrategy strategy) {
305                this.defaultNegotiationStrategy = strategy;
306        }
307
308        /**
309         * Invoked by Spring to inject the ServletContext.
310         */
311        @Override
312        public void setServletContext(ServletContext servletContext) {
313                this.servletContext = servletContext;
314        }
315
316
317        @Override
318        public void afterPropertiesSet() {
319                build();
320        }
321
322        /**
323         * Create and initialize a {@link ContentNegotiationManager} instance.
324         * @since 5.0
325         */
326        @SuppressWarnings("deprecation")
327        public ContentNegotiationManager build() {
328                List<ContentNegotiationStrategy> strategies = new ArrayList<>();
329
330                if (this.strategies != null) {
331                        strategies.addAll(this.strategies);
332                }
333                else {
334                        if (this.favorPathExtension) {
335                                PathExtensionContentNegotiationStrategy strategy;
336                                if (this.servletContext != null && !useRegisteredExtensionsOnly()) {
337                                        strategy = new ServletPathExtensionContentNegotiationStrategy(this.servletContext, this.mediaTypes);
338                                }
339                                else {
340                                        strategy = new PathExtensionContentNegotiationStrategy(this.mediaTypes);
341                                }
342                                strategy.setIgnoreUnknownExtensions(this.ignoreUnknownPathExtensions);
343                                if (this.useRegisteredExtensionsOnly != null) {
344                                        strategy.setUseRegisteredExtensionsOnly(this.useRegisteredExtensionsOnly);
345                                }
346                                strategies.add(strategy);
347                        }
348                        if (this.favorParameter) {
349                                ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(this.mediaTypes);
350                                strategy.setParameterName(this.parameterName);
351                                if (this.useRegisteredExtensionsOnly != null) {
352                                        strategy.setUseRegisteredExtensionsOnly(this.useRegisteredExtensionsOnly);
353                                }
354                                else {
355                                        strategy.setUseRegisteredExtensionsOnly(true);  // backwards compatibility
356                                }
357                                strategies.add(strategy);
358                        }
359                        if (!this.ignoreAcceptHeader) {
360                                strategies.add(new HeaderContentNegotiationStrategy());
361                        }
362                        if (this.defaultNegotiationStrategy != null) {
363                                strategies.add(this.defaultNegotiationStrategy);
364                        }
365                }
366
367                this.contentNegotiationManager = new ContentNegotiationManager(strategies);
368
369                // Ensure media type mappings are available via ContentNegotiationManager#getMediaTypeMappings()
370                // independent of path extension or parameter strategies.
371
372                if (!CollectionUtils.isEmpty(this.mediaTypes) && !this.favorPathExtension && !this.favorParameter) {
373                        this.contentNegotiationManager.addFileExtensionResolvers(
374                                        new MappingMediaTypeFileExtensionResolver(this.mediaTypes));
375                }
376
377                return this.contentNegotiationManager;
378        }
379
380
381        @Override
382        @Nullable
383        public ContentNegotiationManager getObject() {
384                return this.contentNegotiationManager;
385        }
386
387        @Override
388        public Class<?> getObjectType() {
389                return ContentNegotiationManager.class;
390        }
391
392        @Override
393        public boolean isSingleton() {
394                return true;
395        }
396
397}