/* * Copyright 2002-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.web.reactive.accept; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; /** * Factory to create a {@link CompositeContentTypeResolver} and configure it with * one or more {@link RequestedContentTypeResolver} instances with build style * methods. The following table shows methods, resulting strategy instances, and * if in use by default: * * <table> * <tr> * <th>Property Setter</th> * <th>Underlying Strategy</th> * <th>Default Setting</th> * </tr> * <tr> * <td>{@link #favorPathExtension}</td> * <td>{@link PathExtensionContentTypeResolver Path Extension resolver}</td> * <td>On</td> * </tr> * <tr> * <td>{@link #favorParameter}</td> * <td>{@link ParameterContentTypeResolver Parameter resolver}</td> * <td>Off</td> * </tr> * <tr> * <td>{@link #ignoreAcceptHeader}</td> * <td>{@link HeaderContentTypeResolver Header resolver}</td> * <td>Off</td> * </tr> * <tr> * <td>{@link #defaultContentType}</td> * <td>{@link FixedContentTypeResolver Fixed content resolver}</td> * <td>Not set</td> * </tr> * <tr> * <td>{@link #defaultContentTypeResolver}</td> * <td>{@link RequestedContentTypeResolver}</td> * <td>Not set</td> * </tr> * </table> * * <p>The order in which resolvers are configured is fixed. Config methods may * only turn individual resolvers on or off. If you need a custom order for any * reason simply instantiate {@code {@link CompositeContentTypeResolver}} * directly. * * <p>For the path extension and parameter resolvers you may explicitly add * {@link #mediaTypes(Map)}. This will be used to resolve path extensions or a * parameter value such as "json" to a media type such as "application/json". * * <p>The path extension strategy will also use * {@link org.springframework.http.MediaTypeFactory} to resolve a path extension * to a MediaType. * * @author Rossen Stoyanchev * @since 5.0 */ public class RequestedContentTypeResolverBuilder { private boolean favorPathExtension = true; private boolean favorParameter = false; private boolean ignoreAcceptHeader = false; private Map<String, MediaType> mediaTypes = new HashMap<>(); private boolean ignoreUnknownPathExtensions = true; private Boolean useRegisteredExtensionsOnly; private String parameterName = "format"; private RequestedContentTypeResolver contentTypeResolver; /** * Whether the path extension in the URL path should be used to determine * the requested media type. * <p>By default this is set to {@code true} in which case a request * for {@code /hotels.pdf} will be interpreted as a request for * {@code "application/pdf"} regardless of the 'Accept' header. */ public RequestedContentTypeResolverBuilder favorPathExtension(boolean favorPathExtension) { this.favorPathExtension = favorPathExtension; return this; } /** * Add a mapping from a key, extracted from a path extension or a query * parameter, to a MediaType. This is required in order for the parameter * strategy to work. Any extensions explicitly registered here are also * whitelisted for the purpose of Reflected File Download attack detection * (see Spring Framework reference documentation for more details on RFD * attack protection). * <p>The path extension strategy will also use the * {@link org.springframework.http.MediaTypeFactory} to resolve path * extensions. * @param mediaTypes media type mappings */ public RequestedContentTypeResolverBuilder mediaTypes(Map<String, MediaType> mediaTypes) { if (!CollectionUtils.isEmpty(mediaTypes)) { for (Map.Entry<String, MediaType> entry : mediaTypes.entrySet()) { String extension = entry.getKey().toLowerCase(Locale.ENGLISH); this.mediaTypes.put(extension, entry.getValue()); } } return this; } /** * Alternative to {@link #mediaTypes} to add a single mapping. */ public RequestedContentTypeResolverBuilder mediaType(String key, MediaType mediaType) { this.mediaTypes.put(key, mediaType); return this; } /** * Whether to ignore requests with path extension that cannot be resolved * to any media type. Setting this to {@code false} will result in an * {@link org.springframework.web.HttpMediaTypeNotAcceptableException} if * there is no match. * <p>By default this is set to {@code true}. */ public RequestedContentTypeResolverBuilder ignoreUnknownPathExtensions(boolean ignore) { this.ignoreUnknownPathExtensions = ignore; return this; } /** * When {@link #favorPathExtension favorPathExtension} is set, this * property determines whether to use only registered {@code MediaType} mappings * to resolve a path extension to a specific MediaType. * <p>By default this is not set in which case * {@code PathExtensionContentNegotiationStrategy} will use defaults if available. */ public RequestedContentTypeResolverBuilder useRegisteredExtensionsOnly(boolean useRegisteredExtensionsOnly) { this.useRegisteredExtensionsOnly = useRegisteredExtensionsOnly; return this; } /** * Whether a request parameter ("format" by default) should be used to * determine the requested media type. For this option to work you must * register {@link #mediaTypes media type mappings}. * <p>By default this is set to {@code false}. * @see #parameterName */ public RequestedContentTypeResolverBuilder favorParameter(boolean favorParameter) { this.favorParameter = favorParameter; return this; } /** * Set the query parameter name to use when {@link #favorParameter} is on. * <p>The default parameter name is {@code "format"}. */ public RequestedContentTypeResolverBuilder parameterName(String parameterName) { Assert.notNull(parameterName, "parameterName is required"); this.parameterName = parameterName; return this; } /** * Whether to disable checking the 'Accept' request header. * <p>By default this value is set to {@code false}. */ public RequestedContentTypeResolverBuilder ignoreAcceptHeader(boolean ignoreAcceptHeader) { this.ignoreAcceptHeader = ignoreAcceptHeader; return this; } /** * Set the default content type(s) to use when no content type is requested * in order of priority. * * <p>If destinations are present that do not support any of the given media * types, consider appending {@link MediaType#ALL} at the end. * * <p>By default this is not set. * * @see #defaultContentTypeResolver */ public RequestedContentTypeResolverBuilder defaultContentType(MediaType... contentTypes) { this.contentTypeResolver = new FixedContentTypeResolver(Arrays.asList(contentTypes)); return this; } /** * Set a custom {@link RequestedContentTypeResolver} to use to determine * the content type to use when no content type is requested. * <p>By default this is not set. * @see #defaultContentType */ public RequestedContentTypeResolverBuilder defaultContentTypeResolver(RequestedContentTypeResolver resolver) { this.contentTypeResolver = resolver; return this; } public CompositeContentTypeResolver build() { List<RequestedContentTypeResolver> resolvers = new ArrayList<>(); if (this.favorPathExtension) { PathExtensionContentTypeResolver resolver = new PathExtensionContentTypeResolver(this.mediaTypes); resolver.setIgnoreUnknownExtensions(this.ignoreUnknownPathExtensions); if (this.useRegisteredExtensionsOnly != null) { resolver.setUseRegisteredExtensionsOnly(this.useRegisteredExtensionsOnly); } resolvers.add(resolver); } if (this.favorParameter) { ParameterContentTypeResolver resolver = new ParameterContentTypeResolver(this.mediaTypes); resolver.setParameterName(this.parameterName); resolvers.add(resolver); } if (!this.ignoreAcceptHeader) { resolvers.add(new HeaderContentTypeResolver()); } if (this.contentTypeResolver != null) { resolvers.add(this.contentTypeResolver); } return new CompositeContentTypeResolver(resolvers); } }