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}