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}