001/*
002 * Copyright 2002-2018 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.IOException;
020import java.io.InputStream;
021import java.io.OutputStream;
022import java.lang.reflect.ParameterizedType;
023import java.lang.reflect.Type;
024import java.nio.charset.StandardCharsets;
025import java.util.Collection;
026
027import org.springframework.core.io.Resource;
028import org.springframework.core.io.support.ResourceRegion;
029import org.springframework.http.HttpHeaders;
030import org.springframework.http.HttpInputMessage;
031import org.springframework.http.HttpOutputMessage;
032import org.springframework.http.MediaType;
033import org.springframework.http.MediaTypeFactory;
034import org.springframework.lang.Nullable;
035import org.springframework.util.Assert;
036import org.springframework.util.MimeTypeUtils;
037import org.springframework.util.StreamUtils;
038
039/**
040 * Implementation of {@link HttpMessageConverter} that can write a single {@link ResourceRegion},
041 * or Collections of {@link ResourceRegion ResourceRegions}.
042 *
043 * @author Brian Clozel
044 * @author Juergen Hoeller
045 * @since 4.3
046 */
047public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
048
049        public ResourceRegionHttpMessageConverter() {
050                super(MediaType.ALL);
051        }
052
053
054        @Override
055        @SuppressWarnings("unchecked")
056        protected MediaType getDefaultContentType(Object object) {
057                Resource resource = null;
058                if (object instanceof ResourceRegion) {
059                        resource = ((ResourceRegion) object).getResource();
060                }
061                else {
062                        Collection<ResourceRegion> regions = (Collection<ResourceRegion>) object;
063                        if (!regions.isEmpty()) {
064                                resource = regions.iterator().next().getResource();
065                        }
066                }
067                return MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM);
068        }
069
070        @Override
071        public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
072                return false;
073        }
074
075        @Override
076        public boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType) {
077                return false;
078        }
079
080        @Override
081        public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
082                        throws IOException, HttpMessageNotReadableException {
083
084                throw new UnsupportedOperationException();
085        }
086
087        @Override
088        protected ResourceRegion readInternal(Class<?> clazz, HttpInputMessage inputMessage)
089                        throws IOException, HttpMessageNotReadableException {
090
091                throw new UnsupportedOperationException();
092        }
093
094        @Override
095        public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
096                return canWrite(clazz, null, mediaType);
097        }
098
099        @Override
100        public boolean canWrite(@Nullable Type type, @Nullable Class<?> clazz, @Nullable MediaType mediaType) {
101                if (!(type instanceof ParameterizedType)) {
102                        return (type instanceof Class && ResourceRegion.class.isAssignableFrom((Class<?>) type));
103                }
104
105                ParameterizedType parameterizedType = (ParameterizedType) type;
106                if (!(parameterizedType.getRawType() instanceof Class)) {
107                        return false;
108                }
109                Class<?> rawType = (Class<?>) parameterizedType.getRawType();
110                if (!(Collection.class.isAssignableFrom(rawType))) {
111                        return false;
112                }
113                if (parameterizedType.getActualTypeArguments().length != 1) {
114                        return false;
115                }
116                Type typeArgument = parameterizedType.getActualTypeArguments()[0];
117                if (!(typeArgument instanceof Class)) {
118                        return false;
119                }
120
121                Class<?> typeArgumentClass = (Class<?>) typeArgument;
122                return ResourceRegion.class.isAssignableFrom(typeArgumentClass);
123        }
124
125        @Override
126        @SuppressWarnings("unchecked")
127        protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
128                        throws IOException, HttpMessageNotWritableException {
129
130                if (object instanceof ResourceRegion) {
131                        writeResourceRegion((ResourceRegion) object, outputMessage);
132                }
133                else {
134                        Collection<ResourceRegion> regions = (Collection<ResourceRegion>) object;
135                        if (regions.size() == 1) {
136                                writeResourceRegion(regions.iterator().next(), outputMessage);
137                        }
138                        else {
139                                writeResourceRegionCollection((Collection<ResourceRegion>) object, outputMessage);
140                        }
141                }
142        }
143
144
145        protected void writeResourceRegion(ResourceRegion region, HttpOutputMessage outputMessage) throws IOException {
146                Assert.notNull(region, "ResourceRegion must not be null");
147                HttpHeaders responseHeaders = outputMessage.getHeaders();
148
149                long start = region.getPosition();
150                long end = start + region.getCount() - 1;
151                Long resourceLength = region.getResource().contentLength();
152                end = Math.min(end, resourceLength - 1);
153                long rangeLength = end - start + 1;
154                responseHeaders.add("Content-Range", "bytes " + start + '-' + end + '/' + resourceLength);
155                responseHeaders.setContentLength(rangeLength);
156
157                InputStream in = region.getResource().getInputStream();
158                try {
159                        StreamUtils.copyRange(in, outputMessage.getBody(), start, end);
160                }
161                finally {
162                        try {
163                                in.close();
164                        }
165                        catch (IOException ex) {
166                                // ignore
167                        }
168                }
169        }
170
171        private void writeResourceRegionCollection(Collection<ResourceRegion> resourceRegions,
172                        HttpOutputMessage outputMessage) throws IOException {
173
174                Assert.notNull(resourceRegions, "Collection of ResourceRegion should not be null");
175                HttpHeaders responseHeaders = outputMessage.getHeaders();
176
177                MediaType contentType = responseHeaders.getContentType();
178                String boundaryString = MimeTypeUtils.generateMultipartBoundaryString();
179                responseHeaders.set(HttpHeaders.CONTENT_TYPE, "multipart/byteranges; boundary=" + boundaryString);
180                OutputStream out = outputMessage.getBody();
181
182                Resource resource = null;
183                InputStream in = null;
184                long inputStreamPosition = 0;
185
186                try {
187                        for (ResourceRegion region : resourceRegions) {
188                                long start = region.getPosition() - inputStreamPosition;
189                                if (start < 0 || resource != region.getResource()) {
190                                        if (in != null) {
191                                                in.close();
192                                        }
193                                        resource = region.getResource();
194                                        in = resource.getInputStream();
195                                        inputStreamPosition = 0;
196                                        start = region.getPosition();
197                                }
198                                long end = start + region.getCount() - 1;
199                                // Writing MIME header.
200                                println(out);
201                                print(out, "--" + boundaryString);
202                                println(out);
203                                if (contentType != null) {
204                                        print(out, "Content-Type: " + contentType.toString());
205                                        println(out);
206                                }
207                                Long resourceLength = region.getResource().contentLength();
208                                end = Math.min(end, resourceLength - inputStreamPosition - 1);
209                                print(out, "Content-Range: bytes " +
210                                                region.getPosition() + '-' + (region.getPosition() + region.getCount() - 1) +
211                                                '/' + resourceLength);
212                                println(out);
213                                println(out);
214                                // Printing content
215                                StreamUtils.copyRange(in, out, start, end);
216                                inputStreamPosition += (end + 1);
217                        }
218                }
219                finally {
220                        try {
221                                if (in != null) {
222                                        in.close();
223                                }
224                        }
225                        catch (IOException ex) {
226                                // ignore
227                        }
228                }
229
230                println(out);
231                print(out, "--" + boundaryString + "--");
232        }
233
234        private static void println(OutputStream os) throws IOException {
235                os.write('\r');
236                os.write('\n');
237        }
238
239        private static void print(OutputStream os, String buf) throws IOException {
240                os.write(buf.getBytes(StandardCharsets.US_ASCII));
241        }
242
243}