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