DS-3651: Small refactoring

This commit is contained in:
Tom Desair
2017-11-08 14:15:46 +01:00
parent 34e6676129
commit a58e65025c
7 changed files with 120 additions and 106 deletions

View File

@@ -452,7 +452,7 @@ public class BitstreamServiceImpl extends DSpaceObjectServiceImpl<Bitstream> imp
return bitstreamDAO.getNotReferencedBitstreams(context); return bitstreamDAO.getNotReferencedBitstreams(context);
} }
public String getLastModified(Bitstream bitstream) { public Long getLastModified(Bitstream bitstream) {
return bitstreamStorageService.getLastModified(bitstream); return bitstreamStorageService.getLastModified(bitstream);
} }
} }

View File

@@ -203,5 +203,5 @@ public interface BitstreamService extends DSpaceObjectService<Bitstream>, DSpace
List<Bitstream> getNotReferencedBitstreams(Context context) throws SQLException; List<Bitstream> getNotReferencedBitstreams(Context context) throws SQLException;
public String getLastModified(Bitstream bitstream); public Long getLastModified(Bitstream bitstream);
} }

View File

@@ -339,7 +339,7 @@ public class BitstreamStorageServiceImpl implements BitstreamStorageService, Ini
} }
} }
public String getLastModified(Bitstream bitstream) { public Long getLastModified(Bitstream bitstream) {
Map wantedMetadata = new HashMap(); Map wantedMetadata = new HashMap();
wantedMetadata.put("modified", null); wantedMetadata.put("modified", null);
try { try {
@@ -347,7 +347,7 @@ public class BitstreamStorageServiceImpl implements BitstreamStorageService, Ini
} catch (IOException e) { } catch (IOException e) {
log.error(e); log.error(e);
} }
return wantedMetadata.get("modified").toString(); return Long.valueOf(wantedMetadata.get("modified").toString());
} }
/** /**

View File

@@ -7,16 +7,16 @@
*/ */
package org.dspace.storage.bitstore.service; package org.dspace.storage.bitstore.service;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Bitstream;
import org.dspace.core.Context;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Bitstream;
import org.dspace.core.Context;
/** /**
* <P> * <P>
* Stores, retrieves and deletes bitstreams. * Stores, retrieves and deletes bitstreams.
@@ -191,6 +191,11 @@ public interface BitstreamStorageService {
public void migrate(Context context, Integer assetstoreSource, Integer assetstoreDestination, boolean deleteOld, Integer batchCommitSize) throws IOException, SQLException, AuthorizeException; public void migrate(Context context, Integer assetstoreSource, Integer assetstoreDestination, boolean deleteOld, Integer batchCommitSize) throws IOException, SQLException, AuthorizeException;
public String getLastModified(Bitstream bitstream); /**
* Get the last modified timestamp of the file linked to the given bitstream
* @param bitstream The bitstream for which to get the last modified timestamp
* @return The last modified timestamp in milliseconds
*/
public Long getLastModified(Bitstream bitstream);
} }

View File

@@ -9,15 +9,19 @@ package org.dspace.app.rest;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.sql.SQLException;
import java.util.UUID; import java.util.UUID;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils; import org.apache.log4j.Logger;
import org.dspace.app.rest.model.BitstreamRest; import org.dspace.app.rest.model.BitstreamRest;
import org.dspace.app.rest.repository.BitstreamRestRepository; import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.app.rest.utils.MultipartFileSender; import org.dspace.app.rest.utils.MultipartFileSender;
import org.dspace.content.Bitstream;
import org.dspace.content.service.BitstreamService;
import org.dspace.core.Context;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@@ -26,47 +30,54 @@ import org.springframework.web.bind.annotation.RestController;
/** /**
* This is a specialized controller to provide access to the bitstream binary content * This is a specialized controller to provide access to the bitstream binary content
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
* *
*/ */
@RestController @RestController
@RequestMapping("/api/"+BitstreamRest.CATEGORY +"/"+ BitstreamRest.PLURAL_NAME + "/{uuid:[0-9a-fxA-FX]{8}-[0-9a-fxA-FX]{4}-[0-9a-fxA-FX]{4}-[0-9a-fxA-FX]{4}-[0-9a-fxA-FX]{12}}/content") @RequestMapping("/api/"+BitstreamRest.CATEGORY +"/"+ BitstreamRest.PLURAL_NAME + "/{uuid:[0-9a-fxA-FX]{8}-[0-9a-fxA-FX]{4}-[0-9a-fxA-FX]{4}-[0-9a-fxA-FX]{4}-[0-9a-fxA-FX]{12}}/content")
public class BitstreamContentRestController { public class BitstreamContentRestController {
private static final Logger log = Logger.getLogger(BitstreamContentRestController.class);
@Autowired @Autowired
private BitstreamRestRepository bitstreamRestRepository; private BitstreamService bitstreamService;
@RequestMapping(method = {RequestMethod.GET, RequestMethod.HEAD}) @RequestMapping(method = {RequestMethod.GET, RequestMethod.HEAD})
public void retrieve(@PathVariable UUID uuid, HttpServletResponse response, public void retrieve(@PathVariable UUID uuid, HttpServletResponse response,
HttpServletRequest request) throws IOException { HttpServletRequest request) throws IOException, SQLException {
BitstreamRest bit = bitstreamRestRepository.findOne(uuid);
Context context = ContextUtil.obtainContext(request);
Bitstream bit = bitstreamService.find(context, uuid);
if (bit == null) { if (bit == null) {
response.sendError(HttpServletResponse.SC_NOT_FOUND); response.sendError(HttpServletResponse.SC_NOT_FOUND);
return; return;
} }
// Pipe the bits Long lastModified = bitstreamService.getLastModified(bit);
InputStream is = bitstreamRestRepository.retrieve(uuid); String mimetype = bit.getFormat(context).getMIMEType();
long lastModified = bitstreamRestRepository.getLastModified(uuid);
String mimetype = bit.getFormat().getMimetype();
//TODO LOG DOWNLOAD if no range or if last chunk //TODO LOG DOWNLOAD if no range or if last chunk
// Pipe the bits
try(InputStream is = bitstreamService.retrieve(context, bit)) {
MultipartFileSender
.fromInputStream(is)
.withFileName(bit.getName())
.withLength(bit.getSize())
.withChecksum(bit.getChecksum())
.withMimetype(mimetype)
.withLastModified(lastModified)
.with(request)
.with(response)
.serveResource();
//MultipartFileSender
try {
MultipartFileSender.fromBitstream(bit).with(request).with(response).withInputStream(is).withMimetype(mimetype).withLastModified(lastModified).serveResource();
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); log.error(e.getMessage(), e);
} finally { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
IOUtils.closeQuietly(is);
} }
} }

View File

@@ -7,6 +7,14 @@
*/ */
package org.dspace.app.rest.repository; package org.dspace.app.rest.repository;
import java.io.IOException;
import java.io.InputStream;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import org.dspace.app.rest.converter.BitstreamConverter; import org.dspace.app.rest.converter.BitstreamConverter;
import org.dspace.app.rest.model.BitstreamRest; import org.dspace.app.rest.model.BitstreamRest;
import org.dspace.app.rest.model.hateoas.BitstreamResource; import org.dspace.app.rest.model.hateoas.BitstreamResource;
@@ -20,14 +28,6 @@ import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.InputStream;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
/** /**
* This is the repository responsible to manage Bitstream Rest object * This is the repository responsible to manage Bitstream Rest object
* *
@@ -111,14 +111,4 @@ public class BitstreamRestRepository extends DSpaceRestRepository<BitstreamRest,
return is; return is;
} }
public long getLastModified(UUID id) {
Bitstream bit = null;
Context context = obtainContext();
try {
bit = bs.find(context, id);
} catch (SQLException e) {
throw new RuntimeException(e.getMessage(), e);
}
return Long.valueOf(bs.getLastModified(bit));
}
} }

View File

@@ -1,15 +1,8 @@
package org.dspace.app.rest.utils; package org.dspace.app.rest.utils;
import org.apache.commons.lang3.StringUtils; import static java.util.Objects.isNull;
import org.dspace.app.rest.model.BitstreamRest; import static java.util.Objects.nonNull;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@@ -17,8 +10,15 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import static java.util.Objects.isNull; import javax.servlet.ServletOutputStream;
import static java.util.Objects.nonNull; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
@@ -60,17 +60,21 @@ public class MultipartFileSender {
private static final long DEFAULT_EXPIRE_TIME = 60L * 60L * 1000L; private static final long DEFAULT_EXPIRE_TIME = 60L * 60L * 1000L;
//no-cache so request is always performed for logging //no-cache so request is always performed for logging
private static final String CACHE_CONTROL_SETTING = "private, no-cache"; private static final String CACHE_CONTROL_SETTING = "private,no-cache";
BitstreamRest bitstream; private InputStream inputStream;
InputStream inputStream; private HttpServletRequest request;
HttpServletRequest request; private HttpServletResponse response;
HttpServletResponse response; private String contentType;
String contentType; private String disposition;
String disposition = CONTENT_DISPOSITION_INLINE; private long lastModified;
long lastModified; private long length;
private String fileName;
private String checksum;
public MultipartFileSender() { public MultipartFileSender(final InputStream inputStream) {
this.inputStream = inputStream;
ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
String bufferSize = configurationService.getProperty("bitstream-download.buffer.size"); String bufferSize = configurationService.getProperty("bitstream-download.buffer.size");
if (StringUtils.isNotEmpty(bufferSize)) { if (StringUtils.isNotEmpty(bufferSize)) {
@@ -87,14 +91,9 @@ public class MultipartFileSender {
} }
} }
public static MultipartFileSender fromBitstream(BitstreamRest bitstream) {
return new MultipartFileSender().setBitstream(bitstream);
}
//** internal setter **// public static MultipartFileSender fromInputStream(InputStream inputStream) {
private MultipartFileSender setBitstream(BitstreamRest bitstream) { return new MultipartFileSender(inputStream);
this.bitstream = bitstream;
return this;
} }
public MultipartFileSender with(HttpServletRequest httpRequest) { public MultipartFileSender with(HttpServletRequest httpRequest) {
@@ -107,8 +106,18 @@ public class MultipartFileSender {
return this; return this;
} }
public MultipartFileSender withInputStream(InputStream inputStream) { public MultipartFileSender withLength(long length) {
this.inputStream = inputStream; this.length = length;
return this;
}
public MultipartFileSender withFileName(String fileName) {
this.fileName = fileName;
return this;
}
public MultipartFileSender withChecksum(String checksum) {
this.checksum = checksum;
return this; return this;
} }
@@ -129,14 +138,11 @@ public class MultipartFileSender {
} }
if (inputStream == null) { if (inputStream == null) {
log.error("Bitstream has no content"); log.error("Input stream has no content");
response.sendError(HttpServletResponse.SC_NOT_FOUND); response.sendError(HttpServletResponse.SC_NOT_FOUND);
return; return;
} }
Long length = bitstream.getSizeBytes();
String fileName = bitstream.getName();
if (StringUtils.isEmpty(fileName)) { if (StringUtils.isEmpty(fileName)) {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return; return;
@@ -145,9 +151,9 @@ public class MultipartFileSender {
// Validate request headers for caching --------------------------------------------------- // Validate request headers for caching ---------------------------------------------------
// If-None-Match header should contain "*" or ETag. If so, then return 304. // If-None-Match header should contain "*" or ETag. If so, then return 304.
String ifNoneMatch = request.getHeader(IF_NONE_MATCH); String ifNoneMatch = request.getHeader(IF_NONE_MATCH);
if (nonNull(ifNoneMatch) && matches(ifNoneMatch, bitstream.getCheckSum().getValue())) { if (nonNull(ifNoneMatch) && matches(ifNoneMatch, checksum)) {
log.error("If-None-Match header should contain \"*\" or ETag. If so, then return 304."); log.debug("If-None-Match header should contain \"*\" or ETag. If so, then return 304.");
response.setHeader(ETAG, bitstream.getCheckSum().getValue()); // Required in 304. response.setHeader(ETAG, checksum); // Required in 304.
response.sendError(HttpServletResponse.SC_NOT_MODIFIED); response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
return; return;
} }
@@ -156,8 +162,8 @@ public class MultipartFileSender {
// This header is ignored if any If-None-Match header is specified. // This header is ignored if any If-None-Match header is specified.
long ifModifiedSince = request.getDateHeader(IF_MODIFIED_SINCE); long ifModifiedSince = request.getDateHeader(IF_MODIFIED_SINCE);
if (isNull(ifNoneMatch) && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) { if (isNull(ifNoneMatch) && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) {
log.error("If-Modified-Since header should be greater than LastModified. If so, then return 304."); log.debug("If-Modified-Since header should be greater than LastModified. If so, then return 304.");
response.setHeader(ETAG, bitstream.getCheckSum().getValue()); // Required in 304. response.setHeader(ETAG, checksum); // Required in 304.
response.sendError(HttpServletResponse.SC_NOT_MODIFIED); response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
return; return;
} }
@@ -201,18 +207,26 @@ public class MultipartFileSender {
String ifRange = request.getHeader(IF_RANGE); String ifRange = request.getHeader(IF_RANGE);
if (nonNull(ifRange) && !ifRange.equals(fileName)) { if (nonNull(ifRange) && !ifRange.equals(fileName)) {
try { try {
//Assume that the If-Range contains a date
long ifRangeTime = request.getDateHeader(IF_RANGE); // Throws IAE if invalid. long ifRangeTime = request.getDateHeader(IF_RANGE); // Throws IAE if invalid.
if (ifRangeTime != -1) {
if (ifRangeTime == -1 || ifUnmodifiedSince + 1000 <= lastModified) {
//Our file has been updated, send the full range
ranges.add(full); ranges.add(full);
} }
} catch (IllegalArgumentException ignore) { } catch (IllegalArgumentException ignore) {
ranges.add(full); //Assume that the If-Range contains an ETag
if (!matches(ifRange, checksum)) {
//Our file has been updated, send the full range
ranges.add(full);
}
} }
} }
// If any valid If-Range header, then process each part of byte range. // If any valid If-Range header, then process each part of byte range.
if (ranges.isEmpty()) { if (ranges.isEmpty()) {
log.info("If any valid If-Range header, then process each part of byte range."); log.debug("If any valid If-Range header, then process each part of byte range.");
for (String part : range.substring(6).split(",")) { for (String part : range.substring(6).split(",")) {
// Assuming a file with length of 100, the following examples returns bytes at: // 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). // 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
@@ -228,7 +242,7 @@ public class MultipartFileSender {
// Check if Range is syntactically valid. If not, then return 416. // Check if Range is syntactically valid. If not, then return 416.
if (start > end) { if (start > end) {
log.info("Check if Range is syntactically valid. If not, then return 416."); log.warn("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.setHeader(CONTENT_RANGE, String.format(BYTES_DINVALID_BYTE_RANGE_FORMAT, length)); // Required in 416.
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return; return;
@@ -246,7 +260,7 @@ public class MultipartFileSender {
response.setBufferSize(DEFAULT_BUFFER_SIZE); response.setBufferSize(DEFAULT_BUFFER_SIZE);
response.setHeader(CONTENT_TYPE, contentType); response.setHeader(CONTENT_TYPE, contentType);
response.setHeader(ACCEPT_RANGES, BYTES); response.setHeader(ACCEPT_RANGES, BYTES);
response.setHeader(ETAG, bitstream.getCheckSum().getValue()); response.setHeader(ETAG, checksum);
response.setDateHeader(LAST_MODIFIED, lastModified); response.setDateHeader(LAST_MODIFIED, lastModified);
response.setDateHeader(EXPIRES, System.currentTimeMillis() + DEFAULT_EXPIRE_TIME); response.setDateHeader(EXPIRES, System.currentTimeMillis() + DEFAULT_EXPIRE_TIME);
@@ -266,7 +280,6 @@ public class MultipartFileSender {
log.debug("Content-Disposition : {}", disposition); log.debug("Content-Disposition : {}", disposition);
} }
// Content phase // Content phase
if (METHOD_HEAD.equals(request.getMethod())) { if (METHOD_HEAD.equals(request.getMethod())) {
log.debug("HEAD request - skipping content"); log.debug("HEAD request - skipping content");
@@ -275,31 +288,30 @@ public class MultipartFileSender {
// Send requested file (part(s)) to client ------------------------------------------------ // Send requested file (part(s)) to client ------------------------------------------------
// Prepare streams. // Prepare streams.
try (InputStream input = inputStream; try (OutputStream output = response.getOutputStream()) {
OutputStream output = response.getOutputStream()) {
if (ranges.isEmpty() || ranges.get(0) == full) { if (ranges.isEmpty() || ranges.get(0) == full) {
// Return full file. // Return full file.
log.info("Return full file"); log.debug("Return full file");
response.setContentType(contentType); response.setContentType(contentType);
response.setHeader(CONTENT_RANGE, String.format(BYTES_RANGE_FORMAT, full.start, full.end, full.total)); response.setHeader(CONTENT_RANGE, String.format(BYTES_RANGE_FORMAT, full.start, full.end, full.total));
response.setHeader(CONTENT_LENGTH, String.valueOf(full.length)); response.setHeader(CONTENT_LENGTH, String.valueOf(full.length));
Range.copy(input, output, length, full.start, full.length); Range.copy(inputStream, output, length, full.start, full.length);
} else if (ranges.size() == 1) { } else if (ranges.size() == 1) {
// Return single part of file. // Return single part of file.
Range r = ranges.get(0); Range r = ranges.get(0);
log.info("Return 1 part of file : from ({}) to ({})", r.start, r.end); log.debug("Return 1 part of file : from ({}) to ({})", r.start, r.end);
response.setContentType(contentType); response.setContentType(contentType);
response.setHeader(CONTENT_RANGE, String.format(BYTES_RANGE_FORMAT, r.start, r.end, r.total)); response.setHeader(CONTENT_RANGE, String.format(BYTES_RANGE_FORMAT, r.start, r.end, r.total));
response.setHeader(CONTENT_LENGTH, String.valueOf(r.length)); response.setHeader(CONTENT_LENGTH, String.valueOf(r.length));
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206. response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.
// Copy single part range. // Copy single part range.
Range.copy(input, output, length, r.start, r.length); Range.copy(inputStream, output, length, r.start, r.length);
} else { } else {
@@ -312,7 +324,7 @@ public class MultipartFileSender {
// Copy multi part range. // Copy multi part range.
for (Range r : Range.relativize(ranges)) { for (Range r : Range.relativize(ranges)) {
log.info("Return multi part of file : from ({}) to ({})", r.start, r.end); log.debug("Return multi part of file : from ({}) to ({})", r.start, r.end);
// Add multipart boundary and header fields for every range. // Add multipart boundary and header fields for every range.
sos.println(); sos.println();
sos.println("--" + MULTIPART_BOUNDARY); sos.println("--" + MULTIPART_BOUNDARY);
@@ -320,7 +332,7 @@ public class MultipartFileSender {
sos.println(CONTENT_RANGE + ": " + String.format(BYTES_RANGE_FORMAT, r.start, r.end, r.total)); sos.println(CONTENT_RANGE + ": " + String.format(BYTES_RANGE_FORMAT, r.start, r.end, r.total));
// Copy single part range of multi part range. // Copy single part range of multi part range.
Range.copy(input, output, length, r.start, r.length); Range.copy(inputStream, output, length, r.start, r.length);
} }
// End with multipart boundary. // End with multipart boundary.
@@ -352,12 +364,8 @@ public class MultipartFileSender {
*/ */
public Range(long start, long end, long total) { public Range(long start, long end, long total) {
this.start = start; this.start = start;
// if (end <= start + DEFAULT_BUFFER_SIZE) { this.end = Math.min(end, Math.min(start + DEFAULT_BUFFER_SIZE, total - 1));
this.end = end; this.length = this.end - this.start + 1;
// } else {
// this.end = Math.min(start + DEFAULT_BUFFER_SIZE, total - 1);
// }
this.length = this.end - start + 1;
this.total = total; this.total = total;
} }