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.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;
025import javax.servlet.ServletContext;
026
027import org.springframework.beans.factory.FactoryBean;
028import org.springframework.beans.factory.InitializingBean;
029import org.springframework.http.MediaType;
030import org.springframework.util.Assert;
031import org.springframework.util.CollectionUtils;
032import org.springframework.web.context.ServletContextAware;
033
034/**
035 * Factory to create a {@code ContentNegotiationManager} and configure it with
036 * one or more {@link ContentNegotiationStrategy} instances via simple setters.
037 * The following table shows setters, resulting strategy instances, and if in
038 * use by default:
039 *
040 * <table>
041 * <tr>
042 * <th>Property Setter</th>
043 * <th>Underlying Strategy</th>
044 * <th>Default Setting</th>
045 * </tr>
046 * <tr>
047 * <td>{@link #setFavorPathExtension}</td>
048 * <td>{@link PathExtensionContentNegotiationStrategy Path Extension strategy}</td>
049 * <td>On</td>
050 * </tr>
051 * <tr>
052 * <td>{@link #setFavorParameter favorParameter}</td>
053 * <td>{@link ParameterContentNegotiationStrategy Parameter strategy}</td>
054 * <td>Off</td>
055 * </tr>
056 * <tr>
057 * <td>{@link #setIgnoreAcceptHeader ignoreAcceptHeader}</td>
058 * <td>{@link HeaderContentNegotiationStrategy Header strategy}</td>
059 * <td>On</td>
060 * </tr>
061 * <tr>
062 * <td>{@link #setDefaultContentType defaultContentType}</td>
063 * <td>{@link FixedContentNegotiationStrategy Fixed content strategy}</td>
064 * <td>Not set</td>
065 * </tr>
066 * <tr>
067 * <td>{@link #setDefaultContentTypeStrategy defaultContentTypeStrategy}</td>
068 * <td>{@link ContentNegotiationStrategy}</td>
069 * <td>Not set</td>
070 * </tr>
071 * </table>
072 *
073 * <p>The order in which strategies are configured is fixed. Setters may only
074 * turn individual strategies on or off. If you need a custom order for any
075 * reason simply instantiate {@code ContentNegotiationManager} directly.
076 *
077 * <p>For the path extension and parameter strategies you may explicitly add
078 * {@link #setMediaTypes MediaType mappings}. This will be used to resolve path
079 * extensions or a parameter value such as "json" to a media type such as
080 * "application/json".
081 *
082 * <p>The path extension strategy will also use {@link ServletContext#getMimeType}
083 * and the Java Activation framework (JAF), if available, to resolve a path
084 * extension to a MediaType. You may {@link #setUseJaf suppress} the use of JAF.
085 *
086 * @author Rossen Stoyanchev
087 * @author Brian Clozel
088 * @since 3.2
089 */
090public class ContentNegotiationManagerFactoryBean
091                implements FactoryBean<ContentNegotiationManager>, ServletContextAware, InitializingBean {
092
093        private boolean favorPathExtension = true;
094
095        private boolean favorParameter = false;
096
097        private boolean ignoreAcceptHeader = false;
098
099        private Map<String, MediaType> mediaTypes = new HashMap<String, MediaType>();
100
101        private boolean ignoreUnknownPathExtensions = true;
102
103        private Boolean useJaf;
104
105        private String parameterName = "format";
106
107        private ContentNegotiationStrategy defaultNegotiationStrategy;
108
109        private ContentNegotiationManager contentNegotiationManager;
110
111        private ServletContext servletContext;
112
113
114        /**
115         * Whether the path extension in the URL path should be used to determine
116         * the requested media type.
117         * <p>By default this is set to {@code true} in which case a request
118         * for {@code /hotels.pdf} will be interpreted as a request for
119         * {@code "application/pdf"} regardless of the 'Accept' header.
120         */
121        public void setFavorPathExtension(boolean favorPathExtension) {
122                this.favorPathExtension = favorPathExtension;
123        }
124
125        /**
126         * Add a mapping from a key, extracted from a path extension or a query
127         * parameter, to a MediaType. This is required in order for the parameter
128         * strategy to work. Any extensions explicitly registered here are also
129         * whitelisted for the purpose of Reflected File Download attack detection
130         * (see Spring Framework reference documentation for more details on RFD
131         * attack protection).
132         * <p>The path extension strategy will also try to use
133         * {@link ServletContext#getMimeType} and JAF (if present) to resolve path
134         * extensions. To change this behavior see the {@link #useJaf} property.
135         * @param mediaTypes media type mappings
136         * @see #addMediaType(String, MediaType)
137         * @see #addMediaTypes(Map)
138         */
139        public void setMediaTypes(Properties mediaTypes) {
140                if (!CollectionUtils.isEmpty(mediaTypes)) {
141                        for (Map.Entry<Object, Object> entry : mediaTypes.entrySet()) {
142                                String extension = ((String)entry.getKey()).toLowerCase(Locale.ENGLISH);
143                                MediaType mediaType = MediaType.valueOf((String) entry.getValue());
144                                this.mediaTypes.put(extension, mediaType);
145                        }
146                }
147        }
148
149        /**
150         * An alternative to {@link #setMediaTypes} for use in Java code.
151         * @see #setMediaTypes
152         * @see #addMediaTypes
153         */
154        public void addMediaType(String fileExtension, MediaType mediaType) {
155                this.mediaTypes.put(fileExtension, mediaType);
156        }
157
158        /**
159         * An alternative to {@link #setMediaTypes} for use in Java code.
160         * @see #setMediaTypes
161         * @see #addMediaType
162         */
163        public void addMediaTypes(Map<String, MediaType> mediaTypes) {
164                if (mediaTypes != null) {
165                        this.mediaTypes.putAll(mediaTypes);
166                }
167        }
168
169        /**
170         * Whether to ignore requests with path extension that cannot be resolved
171         * to any media type. Setting this to {@code false} will result in an
172         * {@code HttpMediaTypeNotAcceptableException} if there is no match.
173         * <p>By default this is set to {@code true}.
174         */
175        public void setIgnoreUnknownPathExtensions(boolean ignore) {
176                this.ignoreUnknownPathExtensions = ignore;
177        }
178
179        /**
180         * When {@link #setFavorPathExtension favorPathExtension} is set, this
181         * property determines whether to allow use of JAF (Java Activation Framework)
182         * to resolve a path extension to a specific MediaType.
183         * <p>By default this is not set in which case
184         * {@code PathExtensionContentNegotiationStrategy} will use JAF if available.
185         */
186        public void setUseJaf(boolean useJaf) {
187                this.useJaf = useJaf;
188        }
189
190        private boolean isUseJafTurnedOff() {
191                return (this.useJaf != null && !this.useJaf);
192        }
193
194        /**
195         * Whether a request parameter ("format" by default) should be used to
196         * determine the requested media type. For this option to work you must
197         * register {@link #setMediaTypes media type mappings}.
198         * <p>By default this is set to {@code false}.
199         * @see #setParameterName
200         */
201        public void setFavorParameter(boolean favorParameter) {
202                this.favorParameter = favorParameter;
203        }
204
205        /**
206         * Set the query parameter name to use when {@link #setFavorParameter} is on.
207         * <p>The default parameter name is {@code "format"}.
208         */
209        public void setParameterName(String parameterName) {
210                Assert.notNull(parameterName, "parameterName is required");
211                this.parameterName = parameterName;
212        }
213
214        /**
215         * Whether to disable checking the 'Accept' request header.
216         * <p>By default this value is set to {@code false}.
217         */
218        public void setIgnoreAcceptHeader(boolean ignoreAcceptHeader) {
219                this.ignoreAcceptHeader = ignoreAcceptHeader;
220        }
221
222        /**
223         * Set the default content type to use when no content type is requested.
224         * <p>By default this is not set.
225         * @see #setDefaultContentTypeStrategy
226         */
227        public void setDefaultContentType(MediaType contentType) {
228                this.defaultNegotiationStrategy = new FixedContentNegotiationStrategy(contentType);
229        }
230
231        /**
232         * Set a custom {@link ContentNegotiationStrategy} to use to determine
233         * the content type to use when no content type is requested.
234         * <p>By default this is not set.
235         * @see #setDefaultContentType
236         * @since 4.1.2
237         */
238        public void setDefaultContentTypeStrategy(ContentNegotiationStrategy strategy) {
239                this.defaultNegotiationStrategy = strategy;
240        }
241
242        /**
243         * Invoked by Spring to inject the ServletContext.
244         */
245        @Override
246        public void setServletContext(ServletContext servletContext) {
247                this.servletContext = servletContext;
248        }
249
250
251        @Override
252        public void afterPropertiesSet() {
253                List<ContentNegotiationStrategy> strategies = new ArrayList<ContentNegotiationStrategy>();
254
255                if (this.favorPathExtension) {
256                        PathExtensionContentNegotiationStrategy strategy;
257                        if (this.servletContext != null && !isUseJafTurnedOff()) {
258                                strategy = new ServletPathExtensionContentNegotiationStrategy(this.servletContext, this.mediaTypes);
259                        }
260                        else {
261                                strategy = new PathExtensionContentNegotiationStrategy(this.mediaTypes);
262                        }
263                        strategy.setIgnoreUnknownExtensions(this.ignoreUnknownPathExtensions);
264                        if (this.useJaf != null) {
265                                strategy.setUseJaf(this.useJaf);
266                        }
267                        strategies.add(strategy);
268                }
269
270                if (this.favorParameter) {
271                        ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(this.mediaTypes);
272                        strategy.setParameterName(this.parameterName);
273                        strategies.add(strategy);
274                }
275
276                if (!this.ignoreAcceptHeader) {
277                        strategies.add(new HeaderContentNegotiationStrategy());
278                }
279
280                if (this.defaultNegotiationStrategy != null) {
281                        strategies.add(this.defaultNegotiationStrategy);
282                }
283
284                this.contentNegotiationManager = new ContentNegotiationManager(strategies);
285        }
286
287        @Override
288        public ContentNegotiationManager getObject() {
289                return this.contentNegotiationManager;
290        }
291
292        @Override
293        public Class<?> getObjectType() {
294                return ContentNegotiationManager.class;
295        }
296
297        @Override
298        public boolean isSingleton() {
299                return true;
300        }
301
302}