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.Arrays; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.HashMap; 024import java.util.LinkedHashSet; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028import java.util.function.Function; 029 030import org.springframework.http.MediaType; 031import org.springframework.lang.Nullable; 032import org.springframework.util.Assert; 033import org.springframework.util.CollectionUtils; 034import org.springframework.web.HttpMediaTypeNotAcceptableException; 035import org.springframework.web.context.request.NativeWebRequest; 036 037/** 038 * Central class to determine requested {@linkplain MediaType media types} 039 * for a request. This is done by delegating to a list of configured 040 * {@code ContentNegotiationStrategy} instances. 041 * 042 * <p>Also provides methods to look up file extensions for a media type. 043 * This is done by delegating to the list of configured 044 * {@code MediaTypeFileExtensionResolver} instances. 045 * 046 * @author Rossen Stoyanchev 047 * @author Juergen Hoeller 048 * @since 3.2 049 */ 050public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver { 051 052 private final List<ContentNegotiationStrategy> strategies = new ArrayList<>(); 053 054 private final Set<MediaTypeFileExtensionResolver> resolvers = new LinkedHashSet<>(); 055 056 057 /** 058 * Create an instance with the given list of 059 * {@code ContentNegotiationStrategy} strategies each of which may also be 060 * an instance of {@code MediaTypeFileExtensionResolver}. 061 * @param strategies the strategies to use 062 */ 063 public ContentNegotiationManager(ContentNegotiationStrategy... strategies) { 064 this(Arrays.asList(strategies)); 065 } 066 067 /** 068 * A collection-based alternative to 069 * {@link #ContentNegotiationManager(ContentNegotiationStrategy...)}. 070 * @param strategies the strategies to use 071 * @since 3.2.2 072 */ 073 public ContentNegotiationManager(Collection<ContentNegotiationStrategy> strategies) { 074 Assert.notEmpty(strategies, "At least one ContentNegotiationStrategy is expected"); 075 this.strategies.addAll(strategies); 076 for (ContentNegotiationStrategy strategy : this.strategies) { 077 if (strategy instanceof MediaTypeFileExtensionResolver) { 078 this.resolvers.add((MediaTypeFileExtensionResolver) strategy); 079 } 080 } 081 } 082 083 /** 084 * Create a default instance with a {@link HeaderContentNegotiationStrategy}. 085 */ 086 public ContentNegotiationManager() { 087 this(new HeaderContentNegotiationStrategy()); 088 } 089 090 091 /** 092 * Return the configured content negotiation strategies. 093 * @since 3.2.16 094 */ 095 public List<ContentNegotiationStrategy> getStrategies() { 096 return this.strategies; 097 } 098 099 /** 100 * Find a {@code ContentNegotiationStrategy} of the given type. 101 * @param strategyType the strategy type 102 * @return the first matching strategy, or {@code null} if none 103 * @since 4.3 104 */ 105 @SuppressWarnings("unchecked") 106 @Nullable 107 public <T extends ContentNegotiationStrategy> T getStrategy(Class<T> strategyType) { 108 for (ContentNegotiationStrategy strategy : getStrategies()) { 109 if (strategyType.isInstance(strategy)) { 110 return (T) strategy; 111 } 112 } 113 return null; 114 } 115 116 /** 117 * Register more {@code MediaTypeFileExtensionResolver} instances in addition 118 * to those detected at construction. 119 * @param resolvers the resolvers to add 120 */ 121 public void addFileExtensionResolvers(MediaTypeFileExtensionResolver... resolvers) { 122 Collections.addAll(this.resolvers, resolvers); 123 } 124 125 @Override 126 public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException { 127 for (ContentNegotiationStrategy strategy : this.strategies) { 128 List<MediaType> mediaTypes = strategy.resolveMediaTypes(request); 129 if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) { 130 continue; 131 } 132 return mediaTypes; 133 } 134 return MEDIA_TYPE_ALL_LIST; 135 } 136 137 @Override 138 public List<String> resolveFileExtensions(MediaType mediaType) { 139 return doResolveExtensions(resolver -> resolver.resolveFileExtensions(mediaType)); 140 } 141 142 /** 143 * {@inheritDoc} 144 * <p>At startup this method returns extensions explicitly registered with 145 * either {@link PathExtensionContentNegotiationStrategy} or 146 * {@link ParameterContentNegotiationStrategy}. At runtime if there is a 147 * "path extension" strategy and its 148 * {@link PathExtensionContentNegotiationStrategy#setUseRegisteredExtensionsOnly(boolean) 149 * useRegisteredExtensionsOnly} property is set to "false", the list of extensions may 150 * increase as file extensions are resolved via 151 * {@link org.springframework.http.MediaTypeFactory} and cached. 152 */ 153 @Override 154 public List<String> getAllFileExtensions() { 155 return doResolveExtensions(MediaTypeFileExtensionResolver::getAllFileExtensions); 156 } 157 158 private List<String> doResolveExtensions(Function<MediaTypeFileExtensionResolver, List<String>> extractor) { 159 List<String> result = null; 160 for (MediaTypeFileExtensionResolver resolver : this.resolvers) { 161 List<String> extensions = extractor.apply(resolver); 162 if (CollectionUtils.isEmpty(extensions)) { 163 continue; 164 } 165 result = (result != null ? result : new ArrayList<>(4)); 166 for (String extension : extensions) { 167 if (!result.contains(extension)) { 168 result.add(extension); 169 } 170 } 171 } 172 return (result != null ? result : Collections.emptyList()); 173 } 174 175 /** 176 * Return all registered lookup key to media type mappings by iterating 177 * {@link MediaTypeFileExtensionResolver}s. 178 * @since 5.2.4 179 */ 180 public Map<String, MediaType> getMediaTypeMappings() { 181 Map<String, MediaType> result = null; 182 for (MediaTypeFileExtensionResolver resolver : this.resolvers) { 183 if (resolver instanceof MappingMediaTypeFileExtensionResolver) { 184 Map<String, MediaType> map = ((MappingMediaTypeFileExtensionResolver) resolver).getMediaTypes(); 185 if (CollectionUtils.isEmpty(map)) { 186 continue; 187 } 188 result = (result != null ? result : new HashMap<>(4)); 189 result.putAll(map); 190 } 191 } 192 return (result != null ? result : Collections.emptyMap()); 193 } 194 195}