/* * Copyright 2002-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.http.converter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.util.Collection; import org.springframework.core.io.Resource; import org.springframework.core.io.support.ResourceRegion; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; import org.springframework.http.MediaTypeFactory; import org.springframework.util.Assert; import org.springframework.util.MimeTypeUtils; import org.springframework.util.StreamUtils; /** * Implementation of {@link HttpMessageConverter} that can write a single {@link ResourceRegion}, * or Collections of {@link ResourceRegion ResourceRegions}. * * @author Brian Clozel * @author Juergen Hoeller * @since 4.3 */ public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> { public ResourceRegionHttpMessageConverter() { super(MediaType.ALL); } @Override @SuppressWarnings("unchecked") protected MediaType getDefaultContentType(Object object) { Resource resource = null; if (object instanceof ResourceRegion) { resource = ((ResourceRegion) object).getResource(); } else { Collection<ResourceRegion> regions = (Collection<ResourceRegion>) object; if (!regions.isEmpty()) { resource = regions.iterator().next().getResource(); } } return MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM); } @Override public boolean canRead(Class<?> clazz, MediaType mediaType) { return false; } @Override public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) { return false; } @Override public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { throw new UnsupportedOperationException(); } @Override protected ResourceRegion readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { throw new UnsupportedOperationException(); } @Override public boolean canWrite(Class<?> clazz, MediaType mediaType) { return canWrite(clazz, null, mediaType); } @Override public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) { if (!(type instanceof ParameterizedType)) { return ResourceRegion.class.isAssignableFrom((Class<?>) type); } ParameterizedType parameterizedType = (ParameterizedType) type; if (!(parameterizedType.getRawType() instanceof Class)) { return false; } Class<?> rawType = (Class<?>) parameterizedType.getRawType(); if (!(Collection.class.isAssignableFrom(rawType))) { return false; } if (parameterizedType.getActualTypeArguments().length != 1) { return false; } Type typeArgument = parameterizedType.getActualTypeArguments()[0]; if (!(typeArgument instanceof Class)) { return false; } Class<?> typeArgumentClass = (Class<?>) typeArgument; return typeArgumentClass.isAssignableFrom(ResourceRegion.class); } @Override @SuppressWarnings("unchecked") protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { if (object instanceof ResourceRegion) { writeResourceRegion((ResourceRegion) object, outputMessage); } else { Collection<ResourceRegion> regions = (Collection<ResourceRegion>) object; if (regions.size() == 1) { writeResourceRegion(regions.iterator().next(), outputMessage); } else { writeResourceRegionCollection((Collection<ResourceRegion>) object, outputMessage); } } } protected void writeResourceRegion(ResourceRegion region, HttpOutputMessage outputMessage) throws IOException { Assert.notNull(region, "ResourceRegion must not be null"); HttpHeaders responseHeaders = outputMessage.getHeaders(); long start = region.getPosition(); long end = start + region.getCount() - 1; Long resourceLength = region.getResource().contentLength(); end = Math.min(end, resourceLength - 1); long rangeLength = end - start + 1; responseHeaders.add("Content-Range", "bytes " + start + '-' + end + '/' + resourceLength); responseHeaders.setContentLength(rangeLength); InputStream in = region.getResource().getInputStream(); try { StreamUtils.copyRange(in, outputMessage.getBody(), start, end); } finally { try { in.close(); } catch (IOException ex) { // ignore } } } private void writeResourceRegionCollection(Collection<ResourceRegion> resourceRegions, HttpOutputMessage outputMessage) throws IOException { Assert.notNull(resourceRegions, "Collection of ResourceRegion should not be null"); HttpHeaders responseHeaders = outputMessage.getHeaders(); MediaType contentType = responseHeaders.getContentType(); String boundaryString = MimeTypeUtils.generateMultipartBoundaryString(); responseHeaders.set(HttpHeaders.CONTENT_TYPE, "multipart/byteranges; boundary=" + boundaryString); OutputStream out = outputMessage.getBody(); for (ResourceRegion region : resourceRegions) { long start = region.getPosition(); long end = start + region.getCount() - 1; InputStream in = region.getResource().getInputStream(); try { // Writing MIME header. println(out); print(out, "--" + boundaryString); println(out); if (contentType != null) { print(out, "Content-Type: " + contentType.toString()); println(out); } Long resourceLength = region.getResource().contentLength(); end = Math.min(end, resourceLength - 1); print(out, "Content-Range: bytes " + start + '-' + end + '/' + resourceLength); println(out); println(out); // Printing content StreamUtils.copyRange(in, out, start, end); } finally { try { in.close(); } catch (IOException ex) { // ignore } } } println(out); print(out, "--" + boundaryString + "--"); } private static void println(OutputStream os) throws IOException { os.write('\r'); os.write('\n'); } private static void print(OutputStream os, String buf) throws IOException { os.write(buf.getBytes(StandardCharsets.US_ASCII)); } }