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.http.converter;
018
019import java.io.FileNotFoundException;
020import java.io.IOException;
021import java.io.InputStream;
022
023import org.springframework.core.io.ByteArrayResource;
024import org.springframework.core.io.InputStreamResource;
025import org.springframework.core.io.Resource;
026import org.springframework.http.HttpInputMessage;
027import org.springframework.http.HttpOutputMessage;
028import org.springframework.http.MediaType;
029import org.springframework.http.MediaTypeFactory;
030import org.springframework.lang.Nullable;
031import org.springframework.util.StreamUtils;
032
033/**
034 * Implementation of {@link HttpMessageConverter} that can read/write {@link Resource Resources}
035 * and supports byte range requests.
036 *
037 * <p>By default, this converter can read all media types. The {@link MediaTypeFactory} is used
038 * to determine the {@code Content-Type} of written resources.
039 *
040 * @author Arjen Poutsma
041 * @author Juergen Hoeller
042 * @author Kazuki Shimizu
043 * @since 3.0.2
044 */
045public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<Resource> {
046
047        private final boolean supportsReadStreaming;
048
049
050        /**
051         * Create a new instance of the {@code ResourceHttpMessageConverter}
052         * that supports read streaming, i.e. can convert an
053         * {@code HttpInputMessage} to {@code InputStreamResource}.
054         */
055        public ResourceHttpMessageConverter() {
056                super(MediaType.ALL);
057                this.supportsReadStreaming = true;
058        }
059
060        /**
061         * Create a new instance of the {@code ResourceHttpMessageConverter}.
062         * @param supportsReadStreaming whether the converter should support
063         * read streaming, i.e. convert to {@code InputStreamResource}
064         * @since 5.0
065         */
066        public ResourceHttpMessageConverter(boolean supportsReadStreaming) {
067                super(MediaType.ALL);
068                this.supportsReadStreaming = supportsReadStreaming;
069        }
070
071
072        @Override
073        protected boolean supports(Class<?> clazz) {
074                return Resource.class.isAssignableFrom(clazz);
075        }
076
077        @Override
078        protected Resource readInternal(Class<? extends Resource> clazz, HttpInputMessage inputMessage)
079                        throws IOException, HttpMessageNotReadableException {
080
081                if (this.supportsReadStreaming && InputStreamResource.class == clazz) {
082                        return new InputStreamResource(inputMessage.getBody()) {
083                                @Override
084                                public String getFilename() {
085                                        return inputMessage.getHeaders().getContentDisposition().getFilename();
086                                }
087                                @Override
088                                public long contentLength() throws IOException {
089                                        long length = inputMessage.getHeaders().getContentLength();
090                                        return (length != -1 ? length : super.contentLength());
091                                }
092                        };
093                }
094                else if (Resource.class == clazz || ByteArrayResource.class.isAssignableFrom(clazz)) {
095                        byte[] body = StreamUtils.copyToByteArray(inputMessage.getBody());
096                        return new ByteArrayResource(body) {
097                                @Override
098                                @Nullable
099                                public String getFilename() {
100                                        return inputMessage.getHeaders().getContentDisposition().getFilename();
101                                }
102                        };
103                }
104                else {
105                        throw new HttpMessageNotReadableException("Unsupported resource class: " + clazz, inputMessage);
106                }
107        }
108
109        @Override
110        protected MediaType getDefaultContentType(Resource resource) {
111                return MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM);
112        }
113
114        @Override
115        protected Long getContentLength(Resource resource, @Nullable MediaType contentType) throws IOException {
116                // Don't try to determine contentLength on InputStreamResource - cannot be read afterwards...
117                // Note: custom InputStreamResource subclasses could provide a pre-calculated content length!
118                if (InputStreamResource.class == resource.getClass()) {
119                        return null;
120                }
121                long contentLength = resource.contentLength();
122                return (contentLength < 0 ? null : contentLength);
123        }
124
125        @Override
126        protected void writeInternal(Resource resource, HttpOutputMessage outputMessage)
127                        throws IOException, HttpMessageNotWritableException {
128
129                writeContent(resource, outputMessage);
130        }
131
132        protected void writeContent(Resource resource, HttpOutputMessage outputMessage)
133                        throws IOException, HttpMessageNotWritableException {
134                try {
135                        InputStream in = resource.getInputStream();
136                        try {
137                                StreamUtils.copy(in, outputMessage.getBody());
138                        }
139                        catch (NullPointerException ex) {
140                                // ignore, see SPR-13620
141                        }
142                        finally {
143                                try {
144                                        in.close();
145                                }
146                                catch (Throwable ex) {
147                                        // ignore, see SPR-12999
148                                }
149                        }
150                }
151                catch (FileNotFoundException ex) {
152                        // ignore, see SPR-12999
153                }
154        }
155
156}