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}