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.io.IOException;
020import java.io.InputStream;
021import java.util.Locale;
022import java.util.Map;
023import javax.activation.FileTypeMap;
024import javax.activation.MimetypesFileTypeMap;
025import javax.servlet.http.HttpServletRequest;
026
027import org.apache.commons.logging.Log;
028import org.apache.commons.logging.LogFactory;
029
030import org.springframework.core.io.ClassPathResource;
031import org.springframework.core.io.Resource;
032import org.springframework.http.MediaType;
033import org.springframework.util.Assert;
034import org.springframework.util.ClassUtils;
035import org.springframework.util.StringUtils;
036import org.springframework.web.HttpMediaTypeNotAcceptableException;
037import org.springframework.web.context.request.NativeWebRequest;
038import org.springframework.web.util.UriUtils;
039import org.springframework.web.util.UrlPathHelper;
040
041/**
042 * A {@code ContentNegotiationStrategy} that resolves the file extension in the
043 * request path to a key to be used to look up a media type.
044 *
045 * <p>If the file extension is not found in the explicit registrations provided
046 * to the constructor, the Java Activation Framework (JAF) is used as a fallback
047 * mechanism.
048 *
049 * <p>The presence of the JAF is detected and enabled automatically but the
050 * {@link #setUseJaf(boolean)} property may be set to false.
051 *
052 * @author Rossen Stoyanchev
053 * @since 3.2
054 */
055public class PathExtensionContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy {
056
057        private static final boolean JAF_PRESENT = ClassUtils.isPresent("javax.activation.FileTypeMap",
058                        PathExtensionContentNegotiationStrategy.class.getClassLoader());
059
060        private static final Log logger = LogFactory.getLog(PathExtensionContentNegotiationStrategy.class);
061
062        private UrlPathHelper urlPathHelper = new UrlPathHelper();
063
064        private boolean useJaf = true;
065
066        private boolean ignoreUnknownExtensions = true;
067
068
069        /**
070         * Create an instance without any mappings to start with. Mappings may be added
071         * later on if any extensions are resolved through the Java Activation framework.
072         */
073        public PathExtensionContentNegotiationStrategy() {
074                this(null);
075        }
076
077        /**
078         * Create an instance with the given map of file extensions and media types.
079         */
080        public PathExtensionContentNegotiationStrategy(Map<String, MediaType> mediaTypes) {
081                super(mediaTypes);
082                this.urlPathHelper.setUrlDecode(false);
083        }
084
085
086        /**
087         * Configure a {@code UrlPathHelper} to use in {@link #getMediaTypeKey}
088         * in order to derive the lookup path for a target request URL path.
089         * @since 4.2.8
090         */
091        public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
092                this.urlPathHelper = urlPathHelper;
093        }
094
095        /**
096         * Whether to use the Java Activation Framework to look up file extensions.
097         * <p>By default this is set to "true" but depends on JAF being present.
098         */
099        public void setUseJaf(boolean useJaf) {
100                this.useJaf = useJaf;
101        }
102
103        /**
104         * Whether to ignore requests with unknown file extension. Setting this to
105         * {@code false} results in {@code HttpMediaTypeNotAcceptableException}.
106         * <p>By default this is set to {@code true}.
107         */
108        public void setIgnoreUnknownExtensions(boolean ignoreUnknownExtensions) {
109                this.ignoreUnknownExtensions = ignoreUnknownExtensions;
110        }
111
112
113        @Override
114        protected String getMediaTypeKey(NativeWebRequest webRequest) {
115                HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
116                if (request == null) {
117                        logger.warn("An HttpServletRequest is required to determine the media type key");
118                        return null;
119                }
120                String path = this.urlPathHelper.getLookupPathForRequest(request);
121                String extension = UriUtils.extractFileExtension(path);
122                return (StringUtils.hasText(extension) ? extension.toLowerCase(Locale.ENGLISH) : null);
123        }
124
125        @Override
126        protected MediaType handleNoMatch(NativeWebRequest webRequest, String extension)
127                        throws HttpMediaTypeNotAcceptableException {
128
129                if (this.useJaf && JAF_PRESENT) {
130                        MediaType mediaType = ActivationMediaTypeFactory.getMediaType("file." + extension);
131                        if (mediaType != null && !MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) {
132                                return mediaType;
133                        }
134                }
135                if (this.ignoreUnknownExtensions) {
136                        return null;
137                }
138                throw new HttpMediaTypeNotAcceptableException(getAllMediaTypes());
139        }
140
141        /**
142         * A public method exposing the knowledge of the path extension strategy to
143         * resolve file extensions to a {@link MediaType} in this case for a given
144         * {@link Resource}. The method first looks up any explicitly registered
145         * file extensions first and then falls back on JAF if available.
146         * @param resource the resource to look up
147         * @return the MediaType for the extension, or {@code null} if none found
148         * @since 4.3
149         */
150        public MediaType getMediaTypeForResource(Resource resource) {
151                Assert.notNull(resource, "Resource must not be null");
152                MediaType mediaType = null;
153                String filename = resource.getFilename();
154                String extension = StringUtils.getFilenameExtension(filename);
155                if (extension != null) {
156                        mediaType = lookupMediaType(extension);
157                }
158                if (mediaType == null && JAF_PRESENT) {
159                        mediaType = ActivationMediaTypeFactory.getMediaType(filename);
160                }
161                if (MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) {
162                        mediaType = null;
163                }
164                return mediaType;
165        }
166
167
168        /**
169         * Inner class to avoid hard-coded dependency on JAF.
170         */
171        private static class ActivationMediaTypeFactory {
172
173                private static final FileTypeMap fileTypeMap;
174
175                static {
176                        fileTypeMap = initFileTypeMap();
177                }
178
179                /**
180                 * Find extended mime.types from the spring-context-support module.
181                 */
182                private static FileTypeMap initFileTypeMap() {
183                        Resource resource = new ClassPathResource("org/springframework/mail/javamail/mime.types");
184                        if (resource.exists()) {
185                                if (logger.isTraceEnabled()) {
186                                        logger.trace("Loading JAF FileTypeMap from " + resource);
187                                }
188                                InputStream inputStream = null;
189                                try {
190                                        inputStream = resource.getInputStream();
191                                        return new MimetypesFileTypeMap(inputStream);
192                                }
193                                catch (IOException ex) {
194                                        // ignore
195                                }
196                                finally {
197                                        if (inputStream != null) {
198                                                try {
199                                                        inputStream.close();
200                                                }
201                                                catch (IOException ex) {
202                                                        // ignore
203                                                }
204                                        }
205                                }
206                        }
207                        if (logger.isTraceEnabled()) {
208                                logger.trace("Loading default Java Activation Framework FileTypeMap");
209                        }
210                        return FileTypeMap.getDefaultFileTypeMap();
211                }
212
213                public static MediaType getMediaType(String filename) {
214                        String mediaType = fileTypeMap.getContentType(filename);
215                        return (StringUtils.hasText(mediaType) ? MediaType.parseMediaType(mediaType) : null);
216                }
217        }
218
219}