All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.http.codec.ResourceHttpMessageWriter Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2002-2023 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
 *
 *      https://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.codec;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Hints;
import org.springframework.core.codec.ResourceDecoder;
import org.springframework.core.codec.ResourceEncoder;
import org.springframework.core.codec.ResourceRegionEncoder;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.support.ResourceRegion;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpLogging;
import org.springframework.http.HttpRange;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.ZeroCopyHttpOutputMessage;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.MimeTypeUtils;

/**
 * {@code HttpMessageWriter} that can write a {@link Resource}.
 *
 * 

Also an implementation of {@code HttpMessageWriter} with support for writing one * or more {@link ResourceRegion}'s based on the HTTP ranges specified in the request. * *

For reading to a Resource, use {@link ResourceDecoder} wrapped with * {@link DecoderHttpMessageReader}. * * @author Arjen Poutsma * @author Brian Clozel * @author Rossen Stoyanchev * @since 5.0 * @see ResourceEncoder * @see ResourceRegionEncoder * @see HttpRange */ public class ResourceHttpMessageWriter implements HttpMessageWriter { private static final ResolvableType REGION_TYPE = ResolvableType.forClass(ResourceRegion.class); private static final Log logger = HttpLogging.forLogName(ResourceHttpMessageWriter.class); private final ResourceEncoder encoder; private final ResourceRegionEncoder regionEncoder; private final List mediaTypes; public ResourceHttpMessageWriter() { this(ResourceEncoder.DEFAULT_BUFFER_SIZE); } public ResourceHttpMessageWriter(int bufferSize) { this.encoder = new ResourceEncoder(bufferSize); this.regionEncoder = new ResourceRegionEncoder(bufferSize); this.mediaTypes = MediaType.asMediaTypes(this.encoder.getEncodableMimeTypes()); } @Override public boolean canWrite(ResolvableType elementType, @Nullable MediaType mediaType) { return this.encoder.canEncode(elementType, mediaType); } @Override public List getWritableMediaTypes() { return this.mediaTypes; } // Client or server: single Resource... @Override public Mono write(Publisher inputStream, ResolvableType elementType, @Nullable MediaType mediaType, ReactiveHttpOutputMessage message, Map hints) { return Mono.from(inputStream).flatMap(resource -> writeResource(resource, elementType, mediaType, message, hints)); } private Mono writeResource(Resource resource, ResolvableType type, @Nullable MediaType mediaType, ReactiveHttpOutputMessage message, Map hints) { return addDefaultHeaders(message, resource, mediaType, hints) .then(Mono.defer(() -> { Mono result = zeroCopy(resource, null, message, hints); if (result != null) { return result; } else { Mono input = Mono.just(resource); DataBufferFactory factory = message.bufferFactory(); Flux body = this.encoder.encode(input, factory, type, message.getHeaders().getContentType(), hints) .subscribeOn(Schedulers.boundedElastic()); if (logger.isDebugEnabled()) { body = body.doOnNext(buffer -> Hints.touchDataBuffer(buffer, hints, logger)); } return message.writeWith(body); } })); } /** * Adds the default headers for the given resource to the given message. * @since 6.1 */ public Mono addDefaultHeaders(ReactiveHttpOutputMessage message, Resource resource, @Nullable MediaType contentType, Map hints) { return Mono.defer(() -> { HttpHeaders headers = message.getHeaders(); MediaType resourceMediaType = getResourceMediaType(contentType, resource, hints); headers.setContentType(resourceMediaType); if (message instanceof ServerHttpResponse) { // server side headers.set(HttpHeaders.ACCEPT_RANGES, "bytes"); } if (headers.getContentLength() < 0) { return lengthOf(resource) .flatMap(contentLength -> { headers.setContentLength(contentLength); return Mono.empty(); }); } else { return Mono.empty(); } }); } private static MediaType getResourceMediaType( @Nullable MediaType mediaType, Resource resource, Map hints) { if (mediaType != null && mediaType.isConcrete() && !mediaType.equals(MediaType.APPLICATION_OCTET_STREAM)) { return mediaType; } mediaType = MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM); if (logger.isDebugEnabled() && !Hints.isLoggingSuppressed(hints)) { logger.debug(Hints.getLogPrefix(hints) + "Resource associated with '" + mediaType + "'"); } return mediaType; } private static Mono lengthOf(Resource resource) { // Don't consume InputStream... if (InputStreamResource.class != resource.getClass()) { return Mono.fromCallable(resource::contentLength) .filter(length -> length != -1) .onErrorComplete(IOException.class) .subscribeOn(Schedulers.boundedElastic()); } else { return Mono.empty(); } } @Nullable private static Mono zeroCopy(Resource resource, @Nullable ResourceRegion region, ReactiveHttpOutputMessage message, Map hints) { if (message instanceof ZeroCopyHttpOutputMessage zeroCopyHttpOutputMessage && resource.isFile()) { try { File file = resource.getFile(); long pos = region != null ? region.getPosition() : 0; long count = region != null ? region.getCount() : file.length(); if (logger.isDebugEnabled()) { String formatted = region != null ? "region " + pos + "-" + (count) + " of " : ""; logger.debug(Hints.getLogPrefix(hints) + "Zero-copy " + formatted + "[" + resource + "]"); } return zeroCopyHttpOutputMessage.writeWith(file, pos, count); } catch (IOException ex) { // should not happen } } return null; } // Server-side only: single Resource or sub-regions... @Override public Mono write(Publisher inputStream, @Nullable ResolvableType actualType, ResolvableType elementType, @Nullable MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response, Map hints) { HttpHeaders headers = response.getHeaders(); headers.set(HttpHeaders.ACCEPT_RANGES, "bytes"); List ranges; try { ranges = request.getHeaders().getRange(); } catch (IllegalArgumentException ex) { response.setStatusCode(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE); return response.setComplete(); } return Mono.from(inputStream).flatMap(resource -> { if (ranges.isEmpty()) { return writeResource(resource, elementType, mediaType, response, hints); } response.setStatusCode(HttpStatus.PARTIAL_CONTENT); List regions = HttpRange.toResourceRegions(ranges, resource); MediaType resourceMediaType = getResourceMediaType(mediaType, resource, hints); if (regions.size() == 1){ ResourceRegion region = regions.get(0); headers.setContentType(resourceMediaType); return lengthOf(resource) .flatMap(contentLength -> { long start = region.getPosition(); long end = start + region.getCount() - 1; end = Math.min(end, contentLength - 1); headers.add("Content-Range", "bytes " + start + '-' + end + '/' + contentLength); headers.setContentLength(end - start + 1); return Mono.empty(); }) .then(writeSingleRegion(region, response, hints)); } else { String boundary = MimeTypeUtils.generateMultipartBoundaryString(); MediaType multipartType = MediaType.parseMediaType("multipart/byteranges;boundary=" + boundary); headers.setContentType(multipartType); Map allHints = Hints.merge(hints, ResourceRegionEncoder.BOUNDARY_STRING_HINT, boundary); return encodeAndWriteRegions(Flux.fromIterable(regions), resourceMediaType, response, allHints); } }); } private Mono writeSingleRegion(ResourceRegion region, ReactiveHttpOutputMessage message, Map hints) { Mono result = zeroCopy(region.getResource(), region, message, hints); if (result != null) { return result; } else { Publisher input = Mono.just(region); MediaType mediaType = message.getHeaders().getContentType(); return encodeAndWriteRegions(input, mediaType, message, hints); } } private Mono encodeAndWriteRegions(Publisher publisher, @Nullable MediaType mediaType, ReactiveHttpOutputMessage message, Map hints) { Flux body = this.regionEncoder .encode(publisher, message.bufferFactory(), REGION_TYPE, mediaType,hints) .subscribeOn(Schedulers.boundedElastic()); return message.writeWith(body); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy