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 at007 *008 * https://www.apache.org/licenses/LICENSE-2.0009 *010 * Unless required by applicable law or agreed to in writing, software011 * 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 and014 * limitations under the License.015 */016017package org.springframework.http.converter;018019import 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;026027import 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;038039/**040 * Implementation of {@link HttpMessageConverter} that can write a single {@link ResourceRegion},041 * or Collections of {@link ResourceRegion ResourceRegions}.042 *043 * @author Brian Clozel044 * @author Juergen Hoeller045 * @since 4.3046 */047public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {048049 public ResourceRegionHttpMessageConverter() {050 super(MediaType.ALL);051 }052053054 @Override055 @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 }069070 @Override071 public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {072 return false;073 }074075 @Override076 public boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType) {077 return false;078 }079080 @Override081 public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)082 throws IOException, HttpMessageNotReadableException {083084 throw new UnsupportedOperationException();085 }086087 @Override088 protected ResourceRegion readInternal(Class<?> clazz, HttpInputMessage inputMessage)089 throws IOException, HttpMessageNotReadableException {090091 throw new UnsupportedOperationException();092 }093094 @Override095 public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {096 return canWrite(clazz, null, mediaType);097 }098099 @Override100 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 }104105 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 }120121 Class<?> typeArgumentClass = (Class<?>) typeArgument;122 return ResourceRegion.class.isAssignableFrom(typeArgumentClass);123 }124125 @Override126 @SuppressWarnings("unchecked")127 protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)128 throws IOException, HttpMessageNotWritableException {129130 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 }143144145 protected void writeResourceRegion(ResourceRegion region, HttpOutputMessage outputMessage) throws IOException {146 Assert.notNull(region, "ResourceRegion must not be null");147 HttpHeaders responseHeaders = outputMessage.getHeaders();148149 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);156157 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 // ignore167 }168 }169 }170171 private void writeResourceRegionCollection(Collection<ResourceRegion> resourceRegions,172 HttpOutputMessage outputMessage) throws IOException {173174 Assert.notNull(resourceRegions, "Collection of ResourceRegion should not be null");175 HttpHeaders responseHeaders = outputMessage.getHeaders();176177 MediaType contentType = responseHeaders.getContentType();178 String boundaryString = MimeTypeUtils.generateMultipartBoundaryString();179 responseHeaders.set(HttpHeaders.CONTENT_TYPE, "multipart/byteranges; boundary=" + boundaryString);180 OutputStream out = outputMessage.getBody();181182 Resource resource = null;183 InputStream in = null;184 long inputStreamPosition = 0;185186 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 content215 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 // ignore227 }228 }