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