|
|
|
@@ -2,6 +2,8 @@ package org.dspace.app.rest.utils;
|
|
|
|
|
|
|
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
|
|
import org.dspace.app.rest.model.BitstreamRest;
|
|
|
|
|
import org.dspace.services.ConfigurationService;
|
|
|
|
|
import org.dspace.services.factory.DSpaceServicesFactory;
|
|
|
|
|
import org.slf4j.Logger;
|
|
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
|
|
|
|
|
@@ -11,34 +13,69 @@ import javax.servlet.http.HttpServletResponse;
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
import java.io.InputStream;
|
|
|
|
|
import java.io.OutputStream;
|
|
|
|
|
import java.nio.file.attribute.FileTime;
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
|
import java.time.ZoneId;
|
|
|
|
|
import java.time.ZoneOffset;
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
|
import java.util.Arrays;
|
|
|
|
|
import java.util.Date;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
|
|
|
|
|
import static java.util.Objects.isNull;
|
|
|
|
|
import static java.util.Objects.nonNull;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Created by kevin on 10/02/15.
|
|
|
|
|
* See full code here : https://github.com/davinkevin/Podcast-Server/blob/d927d9b8cb9ea1268af74316cd20b7192ca92da7/src/main/java/lan/dk/podcastserver/utils/multipart/MultipartFileSender.java
|
|
|
|
|
*/
|
|
|
|
|
public class MultipartFileSender {
|
|
|
|
|
|
|
|
|
|
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
|
|
|
|
|
protected final Logger log = LoggerFactory.getLogger(this.getClass());
|
|
|
|
|
|
|
|
|
|
private static final int DEFAULT_BUFFER_SIZE = 20480; // ..bytes = 20KB.
|
|
|
|
|
private static final long DEFAULT_EXPIRE_TIME = 604800000L; // ..ms = 1 week.
|
|
|
|
|
private static final String METHOD_HEAD = "HEAD";
|
|
|
|
|
private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES";
|
|
|
|
|
private static final String CONTENT_TYPE_MULTITYPE_WITH_BOUNDARY = "multipart/byteranges; boundary=" + MULTIPART_BOUNDARY;
|
|
|
|
|
private static final String CONTENT_DISPOSITION_INLINE = "inline";
|
|
|
|
|
private static final String CONTENT_DISPOSITION_ATTACHMENT = "attachment";
|
|
|
|
|
private static final String IF_NONE_MATCH = "If-None-Match";
|
|
|
|
|
private static final String IF_MODIFIED_SINCE = "If-Modified-Since";
|
|
|
|
|
private static final String ETAG = "ETag";
|
|
|
|
|
private static final String IF_MATCH = "If-Match";
|
|
|
|
|
private static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
|
|
|
|
|
private static final String RANGE = "Range";
|
|
|
|
|
private static final String CONTENT_RANGE = "Content-Range";
|
|
|
|
|
private static final String IF_RANGE = "If-Range";
|
|
|
|
|
private static final String CONTENT_TYPE = "Content-Type";
|
|
|
|
|
private static final String ACCEPT_RANGES = "Accept-Ranges";
|
|
|
|
|
private static final String BYTES = "bytes";
|
|
|
|
|
private static final String LAST_MODIFIED = "Last-Modified";
|
|
|
|
|
private static final String EXPIRES = "Expires";
|
|
|
|
|
private static final String APPLICATION_OCTET_STREAM = "application/octet-stream";
|
|
|
|
|
private static final String IMAGE = "image";
|
|
|
|
|
private static final String ACCEPT = "Accept";
|
|
|
|
|
private static final String CONTENT_DISPOSITION = "Content-Disposition";
|
|
|
|
|
private static final String CONTENT_LENGTH = "Content-Length";
|
|
|
|
|
private static final String BYTES_RANGE_FORMAT = "bytes %d-%d/%d";
|
|
|
|
|
private static final String CONTENT_DISPOSITION_FORMAT = "%s;filename=\"%s\"";
|
|
|
|
|
private static final String BYTES_DINVALID_BYTE_RANGE_FORMAT = "bytes */%d";
|
|
|
|
|
private static final String CACHE_CONTROL = "Cache-Control";
|
|
|
|
|
private static int DEFAULT_BUFFER_SIZE = 1000000; // ..bytes = 20KB.
|
|
|
|
|
private static final long DEFAULT_EXPIRE_TIME = 60L * 60L * 1000L; // ..ms = 1 week.
|
|
|
|
|
|
|
|
|
|
//no-cache so request is always performed for logging
|
|
|
|
|
private static final String CACHE_CONTROL_SETTING = "private, no-cache";
|
|
|
|
|
|
|
|
|
|
BitstreamRest bitstream;
|
|
|
|
|
InputStream inputStream;
|
|
|
|
|
HttpServletRequest request;
|
|
|
|
|
HttpServletResponse response;
|
|
|
|
|
String contentType;
|
|
|
|
|
String disposition = CONTENT_DISPOSITION_INLINE;
|
|
|
|
|
long lastModified;
|
|
|
|
|
|
|
|
|
|
public MultipartFileSender() {
|
|
|
|
|
ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
|
|
|
|
|
String bufferSize = configurationService.getProperty("bitstream-download.buffer.size");
|
|
|
|
|
if (StringUtils.isNotEmpty(bufferSize)) {
|
|
|
|
|
DEFAULT_BUFFER_SIZE = Integer.parseInt(bufferSize);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static MultipartFileSender fromBitstream(BitstreamRest bitstream) {
|
|
|
|
@@ -61,17 +98,23 @@ public class MultipartFileSender {
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public MultipartFileSender with(InputStream inputStream) {
|
|
|
|
|
public MultipartFileSender withInputStream(InputStream inputStream) {
|
|
|
|
|
this.inputStream = inputStream;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public MultipartFileSender with(String mimetype) {
|
|
|
|
|
public MultipartFileSender withMimetype(String mimetype) {
|
|
|
|
|
this.contentType = mimetype;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public MultipartFileSender withLastModified(long lastModified) {
|
|
|
|
|
this.lastModified = lastModified;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void serveResource() throws Exception {
|
|
|
|
|
|
|
|
|
|
if (response == null || request == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@@ -84,29 +127,28 @@ public class MultipartFileSender {
|
|
|
|
|
|
|
|
|
|
Long length = bitstream.getSizeBytes();
|
|
|
|
|
String fileName = bitstream.getName();
|
|
|
|
|
FileTime lastModifiedObj = FileTime.fromMillis(new Date().getTime());
|
|
|
|
|
|
|
|
|
|
if (StringUtils.isEmpty(fileName) || lastModifiedObj == null) {
|
|
|
|
|
if (StringUtils.isEmpty(fileName)) {
|
|
|
|
|
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
long lastModified = LocalDateTime.ofInstant(lastModifiedObj.toInstant(), ZoneId.of(ZoneOffset.systemDefault().getId())).toEpochSecond(ZoneOffset.UTC);
|
|
|
|
|
|
|
|
|
|
// Validate request headers for caching ---------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// If-None-Match header should contain "*" or ETag. If so, then return 304.
|
|
|
|
|
String ifNoneMatch = request.getHeader("If-None-Match");
|
|
|
|
|
if (ifNoneMatch != null && HttpUtils.matches(ifNoneMatch, fileName)) {
|
|
|
|
|
response.setHeader("ETag", fileName); // Required in 304.
|
|
|
|
|
response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
String ifNoneMatch = request.getHeader(IF_NONE_MATCH);
|
|
|
|
|
// if (nonNull(ifNoneMatch) && matches(ifNoneMatch, bitstream.getCheckSum().getValue())) {
|
|
|
|
|
// log.error("If-None-Match header should contain \"*\" or ETag. If so, then return 304.");
|
|
|
|
|
// response.setHeader(ETAG, bitstream.getCheckSum().getValue()); // Required in 304.
|
|
|
|
|
// response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
|
|
|
|
|
// return;
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// If-Modified-Since header should be greater than LastModified. If so, then return 304.
|
|
|
|
|
// This header is ignored if any If-None-Match header is specified.
|
|
|
|
|
long ifModifiedSince = request.getDateHeader("If-Modified-Since");
|
|
|
|
|
if (ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) {
|
|
|
|
|
response.setHeader("ETag", fileName); // Required in 304.
|
|
|
|
|
long ifModifiedSince = request.getDateHeader(IF_MODIFIED_SINCE);
|
|
|
|
|
if (isNull(ifNoneMatch) && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) {
|
|
|
|
|
log.error("If-Modified-Since header should be greater than LastModified. If so, then return 304.");
|
|
|
|
|
response.setHeader(ETAG, bitstream.getCheckSum().getValue()); // Required in 304.
|
|
|
|
|
response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@@ -114,15 +156,17 @@ public class MultipartFileSender {
|
|
|
|
|
// Validate request headers for resume ----------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// If-Match header should contain "*" or ETag. If not, then return 412.
|
|
|
|
|
String ifMatch = request.getHeader("If-Match");
|
|
|
|
|
if (ifMatch != null && !HttpUtils.matches(ifMatch, fileName)) {
|
|
|
|
|
String ifMatch = request.getHeader(IF_MATCH);
|
|
|
|
|
if (nonNull(ifMatch) && !matches(ifMatch, fileName)) {
|
|
|
|
|
log.error("If-Match header should contain \"*\" or ETag. If not, then return 412.");
|
|
|
|
|
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If-Unmodified-Since header should be greater than LastModified. If not, then return 412.
|
|
|
|
|
long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since");
|
|
|
|
|
long ifUnmodifiedSince = request.getDateHeader(IF_UNMODIFIED_SINCE);
|
|
|
|
|
if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified) {
|
|
|
|
|
log.error("If-Unmodified-Since header should be greater than LastModified. If not, then return 412.");
|
|
|
|
|
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@@ -134,20 +178,21 @@ public class MultipartFileSender {
|
|
|
|
|
List<Range> ranges = new ArrayList<>();
|
|
|
|
|
|
|
|
|
|
// Validate and process Range and If-Range headers.
|
|
|
|
|
String range = request.getHeader("Range");
|
|
|
|
|
if (range != null) {
|
|
|
|
|
String range = request.getHeader(RANGE);
|
|
|
|
|
if (nonNull(range)) {
|
|
|
|
|
|
|
|
|
|
// Range header should match format "bytes=n-n,n-n,n-n...". If not, then return 416.
|
|
|
|
|
if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
|
|
|
|
|
response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
|
|
|
|
|
log.error("Range header should match format \"bytes=n-n,n-n,n-n...\". If not, then return 416.");
|
|
|
|
|
response.setHeader(CONTENT_RANGE, String.format(BYTES_DINVALID_BYTE_RANGE_FORMAT, length)); // Required in 416.
|
|
|
|
|
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String ifRange = request.getHeader("If-Range");
|
|
|
|
|
if (ifRange != null && !ifRange.equals(fileName)) {
|
|
|
|
|
String ifRange = request.getHeader(IF_RANGE);
|
|
|
|
|
if (nonNull(ifRange) && !ifRange.equals(fileName)) {
|
|
|
|
|
try {
|
|
|
|
|
long ifRangeTime = request.getDateHeader("If-Range"); // Throws IAE if invalid.
|
|
|
|
|
long ifRangeTime = request.getDateHeader(IF_RANGE); // Throws IAE if invalid.
|
|
|
|
|
if (ifRangeTime != -1) {
|
|
|
|
|
ranges.add(full);
|
|
|
|
|
}
|
|
|
|
@@ -158,6 +203,7 @@ public class MultipartFileSender {
|
|
|
|
|
|
|
|
|
|
// If any valid If-Range header, then process each part of byte range.
|
|
|
|
|
if (ranges.isEmpty()) {
|
|
|
|
|
log.info("If any valid If-Range header, then process each part of byte range.");
|
|
|
|
|
for (String part : range.substring(6).split(",")) {
|
|
|
|
|
// Assuming a file with length of 100, the following examples returns bytes at:
|
|
|
|
|
// 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
|
|
|
|
@@ -173,7 +219,8 @@ public class MultipartFileSender {
|
|
|
|
|
|
|
|
|
|
// Check if Range is syntactically valid. If not, then return 416.
|
|
|
|
|
if (start > end) {
|
|
|
|
|
response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
|
|
|
|
|
log.info("Check if Range is syntactically valid. If not, then return 416.");
|
|
|
|
|
response.setHeader(CONTENT_RANGE, String.format(BYTES_DINVALID_BYTE_RANGE_FORMAT, length)); // Required in 416.
|
|
|
|
|
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@@ -184,57 +231,62 @@ public class MultipartFileSender {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Prepare and initialize response --------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// Get content type by file name and set content disposition.
|
|
|
|
|
String disposition = "inline";
|
|
|
|
|
|
|
|
|
|
// If content type is unknown, then set the default value.
|
|
|
|
|
// For all content types, see: http://www.w3schools.com/media/media_mimeref.asp
|
|
|
|
|
// To add new content types, add new mime-mapping entry in web.xml.
|
|
|
|
|
if (contentType == null) {
|
|
|
|
|
contentType = "application/octet-stream";
|
|
|
|
|
} else if (!contentType.startsWith("image")) {
|
|
|
|
|
// Else, expect for images, determine content disposition. If content type is supported by
|
|
|
|
|
// the browser, then set to inline, else attachment which will pop a 'save as' dialogue.
|
|
|
|
|
String accept = request.getHeader("Accept");
|
|
|
|
|
disposition = accept != null && HttpUtils.accepts(accept, contentType) ? "inline" : "attachment";
|
|
|
|
|
}
|
|
|
|
|
logger.debug("Content-Type : {}", contentType);
|
|
|
|
|
log.debug("Content-Type : {}", contentType);
|
|
|
|
|
// Initialize response.
|
|
|
|
|
response.reset();
|
|
|
|
|
response.setBufferSize(DEFAULT_BUFFER_SIZE);
|
|
|
|
|
response.setHeader("Content-Type", contentType);
|
|
|
|
|
response.setHeader("Content-Disposition", disposition + ";filename=\"" + fileName + "\"");
|
|
|
|
|
logger.debug("Content-Disposition : {}", disposition);
|
|
|
|
|
response.setHeader("Accept-Ranges", "bytes");
|
|
|
|
|
response.setHeader("ETag", fileName);
|
|
|
|
|
response.setDateHeader("Last-Modified", lastModified);
|
|
|
|
|
response.setDateHeader("Expires", System.currentTimeMillis() + DEFAULT_EXPIRE_TIME);
|
|
|
|
|
response.setHeader(CONTENT_TYPE, contentType);
|
|
|
|
|
response.setHeader(ACCEPT_RANGES, BYTES);
|
|
|
|
|
response.setHeader(ETAG, bitstream.getCheckSum().getValue());
|
|
|
|
|
response.setDateHeader(LAST_MODIFIED, lastModified);
|
|
|
|
|
response.setDateHeader(EXPIRES, System.currentTimeMillis() + DEFAULT_EXPIRE_TIME);
|
|
|
|
|
|
|
|
|
|
//TODO what does this mean
|
|
|
|
|
response.setHeader(CACHE_CONTROL, "private, no-cache");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (isNullOrEmpty(disposition)) {
|
|
|
|
|
if (contentType == null) {
|
|
|
|
|
contentType = APPLICATION_OCTET_STREAM;
|
|
|
|
|
} else if (!contentType.startsWith(IMAGE)) {
|
|
|
|
|
String accept = request.getHeader(ACCEPT);
|
|
|
|
|
disposition = accept != null && accepts(accept, contentType) ? CONTENT_DISPOSITION_INLINE : CONTENT_DISPOSITION_ATTACHMENT;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response.setHeader(CONTENT_DISPOSITION, String.format(CONTENT_DISPOSITION_FORMAT, disposition, fileName));
|
|
|
|
|
log.debug("Content-Disposition : {}", disposition);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Content phase
|
|
|
|
|
if (METHOD_HEAD.equals(request.getMethod())) {
|
|
|
|
|
log.debug("HEAD request - skipping content");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Send requested file (part(s)) to client ------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// Prepare streams.
|
|
|
|
|
try (InputStream input = inputStream;
|
|
|
|
|
OutputStream output = response.getOutputStream()) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (ranges.isEmpty() || ranges.get(0) == full) {
|
|
|
|
|
|
|
|
|
|
// Return full file.
|
|
|
|
|
logger.info("Return full file");
|
|
|
|
|
log.info("Return full file");
|
|
|
|
|
response.setContentType(contentType);
|
|
|
|
|
response.setHeader("Content-Range", "bytes " + full.start + "-" + full.end + "/" + full.total);
|
|
|
|
|
response.setHeader("Content-Length", String.valueOf(full.length));
|
|
|
|
|
response.setHeader(CONTENT_RANGE, String.format(BYTES_RANGE_FORMAT, full.start, full.end, full.total));
|
|
|
|
|
response.setHeader(CONTENT_LENGTH, String.valueOf(full.length));
|
|
|
|
|
Range.copy(input, output, length, full.start, full.length);
|
|
|
|
|
|
|
|
|
|
} else if (ranges.size() == 1) {
|
|
|
|
|
|
|
|
|
|
// Return single part of file.
|
|
|
|
|
Range r = ranges.get(0);
|
|
|
|
|
logger.info("Return 1 part of file : from ({}) to ({})", r.start, r.end);
|
|
|
|
|
log.info("Return 1 part of file : from ({}) to ({})", r.start, r.end);
|
|
|
|
|
response.setContentType(contentType);
|
|
|
|
|
response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);
|
|
|
|
|
response.setHeader("Content-Length", String.valueOf(r.length));
|
|
|
|
|
response.setHeader(CONTENT_RANGE, String.format(BYTES_RANGE_FORMAT, r.start, r.end, r.total));
|
|
|
|
|
response.setHeader(CONTENT_LENGTH, String.valueOf(r.length));
|
|
|
|
|
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.
|
|
|
|
|
|
|
|
|
|
// Copy single part range.
|
|
|
|
@@ -243,20 +295,20 @@ public class MultipartFileSender {
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
// Return multiple parts of file.
|
|
|
|
|
response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY);
|
|
|
|
|
response.setContentType(CONTENT_TYPE_MULTITYPE_WITH_BOUNDARY);
|
|
|
|
|
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.
|
|
|
|
|
|
|
|
|
|
// Cast back to ServletOutputStream to get the easy println methods.
|
|
|
|
|
ServletOutputStream sos = (ServletOutputStream) output;
|
|
|
|
|
|
|
|
|
|
// Copy multi part range.
|
|
|
|
|
for (Range r : ranges) {
|
|
|
|
|
logger.info("Return multi part of file : from ({}) to ({})", r.start, r.end);
|
|
|
|
|
for (Range r : Range.relativize(ranges)) {
|
|
|
|
|
log.info("Return multi part of file : from ({}) to ({})", r.start, r.end);
|
|
|
|
|
// Add multipart boundary and header fields for every range.
|
|
|
|
|
sos.println();
|
|
|
|
|
sos.println("--" + MULTIPART_BOUNDARY);
|
|
|
|
|
sos.println("Content-Type: " + contentType);
|
|
|
|
|
sos.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total);
|
|
|
|
|
sos.println(CONTENT_TYPE + ": " + contentType);
|
|
|
|
|
sos.println(CONTENT_RANGE + ": " + String.format(BYTES_RANGE_FORMAT, r.start, r.end, r.total));
|
|
|
|
|
|
|
|
|
|
// Copy single part range of multi part range.
|
|
|
|
|
Range.copy(input, output, length, r.start, r.length);
|
|
|
|
@@ -268,8 +320,14 @@ public class MultipartFileSender {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static boolean isNullOrEmpty(String disposition) {
|
|
|
|
|
return !(disposition == null || disposition.length() == 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static class Range {
|
|
|
|
|
long start;
|
|
|
|
|
long end;
|
|
|
|
@@ -278,21 +336,36 @@ public class MultipartFileSender {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Construct a byte range.
|
|
|
|
|
*
|
|
|
|
|
* @param start Start of the byte range.
|
|
|
|
|
* @param end End of the byte range.
|
|
|
|
|
* @param total Total length of the byte source.
|
|
|
|
|
*/
|
|
|
|
|
public Range(long start, long end, long total) {
|
|
|
|
|
this.start = start;
|
|
|
|
|
if (end < start + DEFAULT_BUFFER_SIZE) {
|
|
|
|
|
// if (end <= start + DEFAULT_BUFFER_SIZE) {
|
|
|
|
|
this.end = end;
|
|
|
|
|
} else {
|
|
|
|
|
this.end = Math.min(start + DEFAULT_BUFFER_SIZE, total - 1);
|
|
|
|
|
}
|
|
|
|
|
// } else {
|
|
|
|
|
// this.end = Math.min(start + DEFAULT_BUFFER_SIZE, total - 1);
|
|
|
|
|
// }
|
|
|
|
|
this.length = this.end - start + 1;
|
|
|
|
|
this.total = total;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static List<Range> relativize(List<Range> ranges) {
|
|
|
|
|
|
|
|
|
|
List<Range> builder = new ArrayList<>(ranges.size());
|
|
|
|
|
|
|
|
|
|
Range prevRange = null;
|
|
|
|
|
for (Range r : ranges) {
|
|
|
|
|
Range newRange = isNull(prevRange) ? r : new Range(r.start - prevRange.end - 1, r.end - prevRange.end - 1, r.total);
|
|
|
|
|
builder.add(newRange);
|
|
|
|
|
prevRange = r;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return builder;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static long sublong(String value, int beginIndex, int endIndex) {
|
|
|
|
|
String substring = value.substring(beginIndex, endIndex);
|
|
|
|
|
return (substring.length() > 0) ? Long.parseLong(substring) : -1;
|
|
|
|
@@ -325,15 +398,8 @@ public class MultipartFileSender {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
private static class HttpUtils {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns true if the given accept header accepts the given value.
|
|
|
|
|
* @param acceptHeader The accept header.
|
|
|
|
|
* @param toAccept The value to be accepted.
|
|
|
|
|
* @return True if the given accept header accepts the given value.
|
|
|
|
|
*/
|
|
|
|
|
public static boolean accepts(String acceptHeader, String toAccept) {
|
|
|
|
|
private static boolean accepts(String acceptHeader, String toAccept) {
|
|
|
|
|
String[] acceptValues = acceptHeader.split("\\s*(,|;)\\s*");
|
|
|
|
|
Arrays.sort(acceptValues);
|
|
|
|
|
|
|
|
|
@@ -342,17 +408,10 @@ public class MultipartFileSender {
|
|
|
|
|
|| Arrays.binarySearch(acceptValues, "*/*") > -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns true if the given match header matches the given value.
|
|
|
|
|
* @param matchHeader The match header.
|
|
|
|
|
* @param toMatch The value to be matched.
|
|
|
|
|
* @return True if the given match header matches the given value.
|
|
|
|
|
*/
|
|
|
|
|
public static boolean matches(String matchHeader, String toMatch) {
|
|
|
|
|
private static boolean matches(String matchHeader, String toMatch) {
|
|
|
|
|
String[] matchValues = matchHeader.split("\\s*,\\s*");
|
|
|
|
|
Arrays.sort(matchValues);
|
|
|
|
|
return Arrays.binarySearch(matchValues, toMatch) > -1
|
|
|
|
|
|| Arrays.binarySearch(matchValues, "*") > -1;
|
|
|
|
|
}
|
|
|
|
|
return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|