mirror of
https://github.com/DSpace/DSpace.git
synced 2025-10-07 01:54:22 +00:00
Merge branch 'DS-3651_Range-Header-support' of https://github.com/atmire/DSpace into atmire-DS-3651_Range-Header-support
This commit is contained in:
@@ -453,4 +453,8 @@ public class BitstreamServiceImpl extends DSpaceObjectServiceImpl<Bitstream> imp
|
||||
public List<Bitstream> getNotReferencedBitstreams(Context context) throws SQLException {
|
||||
return bitstreamDAO.getNotReferencedBitstreams(context);
|
||||
}
|
||||
|
||||
public Long getLastModified(Bitstream bitstream) {
|
||||
return bitstreamStorageService.getLastModified(bitstream);
|
||||
}
|
||||
}
|
||||
|
@@ -202,4 +202,6 @@ public interface BitstreamService extends DSpaceObjectService<Bitstream>, DSpace
|
||||
int countBitstreamsWithoutPolicy(Context context) throws SQLException;
|
||||
|
||||
List<Bitstream> getNotReferencedBitstreams(Context context) throws SQLException;
|
||||
|
||||
public Long getLastModified(Bitstream bitstream);
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@
|
||||
|
||||
package org.dspace.google;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
@@ -19,10 +20,11 @@ import org.apache.log4j.Logger;
|
||||
import org.dspace.content.factory.ContentServiceFactory;
|
||||
import org.dspace.core.ConfigurationManager;
|
||||
import org.dspace.core.Constants;
|
||||
import org.dspace.services.factory.DSpaceServicesFactory;
|
||||
import org.dspace.services.ConfigurationService;
|
||||
import org.dspace.services.model.Event;
|
||||
import org.dspace.usage.AbstractUsageEventListener;
|
||||
import org.dspace.usage.UsageEvent;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
@@ -46,14 +48,25 @@ public class GoogleRecorderEventListener extends AbstractUsageEventListener {
|
||||
private CloseableHttpClient httpclient;
|
||||
private String GoogleURL = "https://www.google-analytics.com/collect";
|
||||
private static Logger log = Logger.getLogger(GoogleRecorderEventListener.class);
|
||||
protected ContentServiceFactory contentServiceFactory = ContentServiceFactory.getInstance();
|
||||
|
||||
protected ContentServiceFactory contentServiceFactory;
|
||||
protected ConfigurationService configurationService;
|
||||
|
||||
public GoogleRecorderEventListener() {
|
||||
// httpclient is threadsafe so we only need one.
|
||||
httpclient = HttpClients.createDefault();
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setContentServiceFactory(ContentServiceFactory contentServiceFactory) {
|
||||
this.contentServiceFactory = contentServiceFactory;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setConfigurationService(final ConfigurationService configurationService) {
|
||||
this.configurationService = configurationService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveEvent(Event event) {
|
||||
if((event instanceof UsageEvent))
|
||||
@@ -61,12 +74,9 @@ public class GoogleRecorderEventListener extends AbstractUsageEventListener {
|
||||
log.debug("Usage event received " + event.getName());
|
||||
|
||||
// This is a wee bit messy but these keys should be combined in future.
|
||||
analyticsKey = DSpaceServicesFactory.getInstance().getConfigurationService().getProperty("jspui.google.analytics.key");
|
||||
if (analyticsKey == null ) {
|
||||
analyticsKey = DSpaceServicesFactory.getInstance().getConfigurationService().getProperty("xmlui.google.analytics.key");
|
||||
}
|
||||
analyticsKey = configurationService.getProperty("google.analytics.key");
|
||||
|
||||
if (analyticsKey != null ) {
|
||||
if (StringUtils.isNotBlank(analyticsKey)) {
|
||||
try {
|
||||
UsageEvent ue = (UsageEvent)event;
|
||||
|
||||
|
@@ -10,12 +10,12 @@ package org.dspace.statistics;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.dspace.eperson.EPerson;
|
||||
import org.dspace.services.model.Event;
|
||||
import org.dspace.statistics.factory.StatisticsServiceFactory;
|
||||
import org.dspace.statistics.service.SolrLoggerService;
|
||||
import org.dspace.usage.AbstractUsageEventListener;
|
||||
import org.dspace.usage.UsageEvent;
|
||||
import org.dspace.usage.UsageSearchEvent;
|
||||
import org.dspace.usage.UsageWorkflowEvent;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
@@ -31,8 +31,9 @@ public class SolrLoggerUsageEventListener extends AbstractUsageEventListener {
|
||||
|
||||
protected SolrLoggerService solrLoggerService;
|
||||
|
||||
public SolrLoggerUsageEventListener() {
|
||||
solrLoggerService = StatisticsServiceFactory.getInstance().getSolrLoggerService();
|
||||
@Autowired
|
||||
public void setSolrLoggerService(SolrLoggerService solrLoggerService) {
|
||||
this.solrLoggerService = solrLoggerService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -43,7 +44,7 @@ public class SolrLoggerUsageEventListener extends AbstractUsageEventListener {
|
||||
log.debug("Usage event received " + event.getName());
|
||||
try{
|
||||
UsageEvent ue = (UsageEvent)event;
|
||||
|
||||
|
||||
EPerson currentUser = ue.getContext() == null ? null : ue.getContext().getCurrentUser();
|
||||
|
||||
if(UsageEvent.Action.VIEW == ue.getAction()){
|
||||
@@ -74,7 +75,7 @@ public class SolrLoggerUsageEventListener extends AbstractUsageEventListener {
|
||||
log.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -339,6 +339,17 @@ public class BitstreamStorageServiceImpl implements BitstreamStorageService, Ini
|
||||
}
|
||||
}
|
||||
|
||||
public Long getLastModified(Bitstream bitstream) {
|
||||
Map wantedMetadata = new HashMap();
|
||||
wantedMetadata.put("modified", null);
|
||||
try {
|
||||
wantedMetadata = stores.get(incoming).about(bitstream, wantedMetadata);
|
||||
} catch (IOException e) {
|
||||
log.error(e);
|
||||
}
|
||||
return Long.valueOf(wantedMetadata.get("modified").toString());
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param context
|
||||
|
@@ -190,4 +190,12 @@ public interface BitstreamStorageService {
|
||||
*/
|
||||
public void migrate(Context context, Integer assetstoreSource, Integer assetstoreDestination, boolean deleteOld, Integer batchCommitSize) throws IOException, SQLException, AuthorizeException;
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
}
|
||||
|
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:context="http://www.springframework.org/schema/context"
|
||||
xmlns:util="http://www.springframework.org/schema/util"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans
|
||||
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
|
||||
http://www.springframework.org/schema/context
|
||||
http://www.springframework.org/schema/context/spring-context-3.0.xsd
|
||||
http://www.springframework.org/schema/util
|
||||
http://www.springframework.org/schema/util/spring-util-3.0.xsd"
|
||||
default-autowire-candidates="*Service,*DAO,javax.sql.DataSource">
|
||||
|
||||
<context:annotation-config /> <!-- allows us to use spring annotations in beans -->
|
||||
|
||||
<!-- Run an internal SOLR server for the Discovery search service -->
|
||||
<bean class="org.dspace.discovery.MockSolrServiceImpl" id="org.dspace.discovery.SearchService"/>
|
||||
|
||||
<!-- Run an internal SOLR server for the statistics service -->
|
||||
<bean class="org.dspace.statistics.MockSolrLoggerServiceImpl" id="solrLoggerService" lazy-init="true"/>
|
||||
|
||||
</beans>
|
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
The contents of this file are subject to the license and copyright
|
||||
detailed in the LICENSE and NOTICE files at the root of the source
|
||||
tree and available online at
|
||||
|
||||
http://www.dspace.org/license/
|
||||
|
||||
-->
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:context="http://www.springframework.org/schema/context"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans
|
||||
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
|
||||
http://www.springframework.org/schema/context
|
||||
http://www.springframework.org/schema/context/spring-context-2.5.xsd"
|
||||
default-autowire-candidates="*Service,*DAO,javax.sql.DataSource">
|
||||
|
||||
<context:annotation-config /> <!-- allows us to use spring annotations in beans -->
|
||||
|
||||
<bean class="org.dspace.discovery.MockSolrServiceImpl" id="org.dspace.discovery.SearchService"/>
|
||||
|
||||
<alias name="org.dspace.discovery.SearchService" alias="org.dspace.discovery.IndexingService"/>
|
||||
|
||||
<!--<bean class="org.dspace.discovery.SolrServiceIndexOutputPlugin" id="solrServiceIndexOutputPlugin"/>-->
|
||||
|
||||
<!-- Statistics services are both lazy loaded (by name), as you are likely just using ONE of them and not both -->
|
||||
<bean id="elasticSearchLoggerService" class="org.dspace.statistics.ElasticSearchLoggerServiceImpl" lazy-init="true"/>
|
||||
<bean id="solrLoggerService" class="org.dspace.statistics.MockSolrLoggerServiceImpl" lazy-init="true"/>
|
||||
|
||||
</beans>
|
@@ -9,17 +9,27 @@ package org.dspace.app.rest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.sql.SQLException;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.apache.catalina.connector.ClientAbortException;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.dspace.app.rest.model.BitstreamRest;
|
||||
import org.dspace.app.rest.repository.BitstreamRestRepository;
|
||||
import org.dspace.core.Utils;
|
||||
import org.dspace.app.rest.utils.ContextUtil;
|
||||
import org.dspace.app.rest.utils.MultipartFileSender;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.authorize.service.AuthorizeService;
|
||||
import org.dspace.content.Bitstream;
|
||||
import org.dspace.content.service.BitstreamService;
|
||||
import org.dspace.core.Constants;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.services.EventService;
|
||||
import org.dspace.usage.UsageEvent;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.ResourceSupport;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
@@ -27,45 +37,98 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* This is a specialized controller to provide access to the bitstream binary content
|
||||
*
|
||||
*
|
||||
* @author Andrea Bollini (andrea.bollini at 4science.it)
|
||||
* @author Tom Desair (tom dot desair at atmire dot com)
|
||||
* @author Frederic Van Reet (frederic dot vanreet at atmire dot com)
|
||||
*
|
||||
*/
|
||||
@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")
|
||||
public class BitstreamContentRestController {
|
||||
@Autowired
|
||||
private BitstreamRestRepository bitstreamRestRepository;
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET)
|
||||
public void retrieve(@PathVariable UUID uuid, HttpServletResponse response,
|
||||
HttpServletRequest request) throws IOException {
|
||||
BitstreamRest bit = bitstreamRestRepository.findOne(uuid);
|
||||
if (bit == null) {
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
response.setHeader("ETag", bit.getCheckSum().getValue());
|
||||
response.setContentLengthLong(bit.getSizeBytes());
|
||||
// Check for if-modified-since header
|
||||
long modSince = request.getDateHeader("If-Modified-Since");
|
||||
// we should keep last modification date on the bitstream
|
||||
// if (modSince != -1 && item.getLastModified().getTime() < modSince)
|
||||
// {
|
||||
// // Item has not been modified since requested date,
|
||||
// // hence bitstream has not; return 304
|
||||
// response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Pipe the bits
|
||||
InputStream is = bitstreamRestRepository.retrieve(uuid);
|
||||
|
||||
// Set the response MIME type
|
||||
response.setContentType(bit.getFormat().getMimetype());
|
||||
|
||||
Utils.bufferedCopy(is, response.getOutputStream());
|
||||
is.close();
|
||||
response.getOutputStream().flush();
|
||||
private static final Logger log = Logger.getLogger(BitstreamContentRestController.class);
|
||||
|
||||
//Most file systems are configured to use block sizes of 4096 or 8192 and our buffer should be a multiple of that.
|
||||
private static final int BUFFER_SIZE = 4096 * 10;
|
||||
|
||||
@Autowired
|
||||
private BitstreamService bitstreamService;
|
||||
|
||||
@Autowired
|
||||
private EventService eventService;
|
||||
|
||||
@Autowired
|
||||
private AuthorizeService authorizeService;
|
||||
|
||||
@RequestMapping(method = {RequestMethod.GET, RequestMethod.HEAD})
|
||||
public void retrieve(@PathVariable UUID uuid, HttpServletResponse response,
|
||||
HttpServletRequest request) throws IOException, SQLException, AuthorizeException {
|
||||
|
||||
Context context = ContextUtil.obtainContext(request);
|
||||
|
||||
Bitstream bit = getBitstream(context, uuid, response);
|
||||
if (bit == null) {
|
||||
//The bitstream was not found or we're not authorized to read it.
|
||||
return;
|
||||
}
|
||||
|
||||
Long lastModified = bitstreamService.getLastModified(bit);
|
||||
String mimetype = bit.getFormat(context).getMIMEType();
|
||||
|
||||
// Pipe the bits
|
||||
try(InputStream is = bitstreamService.retrieve(context, bit)) {
|
||||
|
||||
MultipartFileSender sender = MultipartFileSender
|
||||
.fromInputStream(is)
|
||||
.withBufferSize(BUFFER_SIZE)
|
||||
.withFileName(bit.getName())
|
||||
.withLength(bit.getSize())
|
||||
.withChecksum(bit.getChecksum())
|
||||
.withMimetype(mimetype)
|
||||
.withLastModified(lastModified)
|
||||
.with(request)
|
||||
.with(response);
|
||||
|
||||
if (sender.isNoRangeRequest() && isNotAnErrorResponse(response)) {
|
||||
//We only log a download request when serving a request without Range header. This is because
|
||||
//a browser always sends a regular request first to check for Range support.
|
||||
eventService.fireEvent(
|
||||
new UsageEvent(
|
||||
UsageEvent.Action.VIEW,
|
||||
request,
|
||||
context,
|
||||
bit));
|
||||
}
|
||||
|
||||
//We have all the data we need, close the connection to the database so that it doesn't stay open during
|
||||
//download/streaming
|
||||
context.complete();
|
||||
|
||||
//Send the data
|
||||
if (sender.isValid()) {
|
||||
sender.serveResource();
|
||||
}
|
||||
|
||||
} catch(ClientAbortException ex) {
|
||||
log.debug("Client aborted the request before the download was completed. Client is probably switching to a Range request.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Bitstream getBitstream(Context context, @PathVariable UUID uuid, HttpServletResponse response) throws SQLException, IOException, AuthorizeException {
|
||||
Bitstream bit = bitstreamService.find(context, uuid);
|
||||
if (bit == null) {
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
} else {
|
||||
authorizeService.authorizeAction(context, bit, Constants.READ);
|
||||
}
|
||||
|
||||
return bit;
|
||||
}
|
||||
|
||||
private boolean isNotAnErrorResponse(HttpServletResponse response) {
|
||||
Response.Status.Family responseCode = Response.Status.Family.familyOf(response.getStatus());
|
||||
return responseCode.equals(Response.Status.Family.SUCCESSFUL)
|
||||
|| responseCode.equals(Response.Status.Family.REDIRECTION);
|
||||
}
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
package org.dspace.app.rest.configuration;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.MalformedURLException;
|
||||
|
||||
import org.dspace.kernel.config.SpringLoader;
|
||||
import org.dspace.services.ConfigurationService;
|
||||
|
||||
/**
|
||||
* Utility class that will load the Spring XML configuration files related to the REST webapp
|
||||
*
|
||||
* @author Tom Desair (tom dot desair at atmire dot com)
|
||||
* @author Kevin Van de Velde (kevin at atmire dot com)
|
||||
*/
|
||||
public class RESTSpringLoader implements SpringLoader {
|
||||
|
||||
@Override
|
||||
public String[] getResourcePaths(ConfigurationService configurationService) {
|
||||
StringBuffer filePath = new StringBuffer();
|
||||
filePath.append(configurationService.getProperty("dspace.dir"));
|
||||
filePath.append(File.separator);
|
||||
filePath.append("config");
|
||||
filePath.append(File.separator);
|
||||
filePath.append("spring");
|
||||
filePath.append(File.separator);
|
||||
filePath.append("rest");
|
||||
filePath.append(File.separator);
|
||||
|
||||
|
||||
try {
|
||||
return new String[]{new File(filePath.toString()).toURI().toURL().toString() + XML_SUFFIX};
|
||||
} catch (MalformedURLException e) {
|
||||
return new String[0];
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
package org.dspace.app.rest.exception;
|
||||
|
||||
|
||||
import static org.springframework.web.servlet.DispatcherServlet.EXCEPTION_ATTRIBUTE;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Date;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
||||
|
||||
/**
|
||||
* This Controller advice will handle all exceptions thrown by the DSpace API module
|
||||
*
|
||||
* @author Tom Desair (tom dot desair at atmire dot com)
|
||||
* @author Frederic Van Reet (frederic dot vanreet at atmire dot com)
|
||||
* @author Andrea Bollini (andrea.bollini at 4science.it)
|
||||
*
|
||||
*/
|
||||
@ControllerAdvice
|
||||
public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionHandler{
|
||||
|
||||
@ExceptionHandler(AuthorizeException.class)
|
||||
protected void handleAuthorizeException(HttpServletRequest request, HttpServletResponse response, Exception ex) throws IOException {
|
||||
sendErrorResponse(request, response, ex, ex.getMessage(), HttpServletResponse.SC_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@ExceptionHandler(SQLException.class)
|
||||
protected void handleSQLException(HttpServletRequest request, HttpServletResponse response, Exception ex) throws IOException {
|
||||
sendErrorResponse(request, response, ex,
|
||||
"An internal database error occurred", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
@ExceptionHandler(IOException.class)
|
||||
protected void handleIOException(HttpServletRequest request, HttpServletResponse response, Exception ex) throws IOException {
|
||||
sendErrorResponse(request, response, ex,
|
||||
"An internal read or write operation failed (IO Exception)", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
private void sendErrorResponse(final HttpServletRequest request, final HttpServletResponse response,
|
||||
final Exception ex, final String message, final int statusCode) throws IOException {
|
||||
//Make sure Spring picks up this exception
|
||||
request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
|
||||
|
||||
//Exception properties will be set by org.springframework.boot.web.support.ErrorPageFilter
|
||||
response.sendError(statusCode, message);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,474 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
package org.dspace.app.rest.utils;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
import static java.util.Objects.nonNull;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Utility class to send an input stream with Range header and ETag support.
|
||||
* Based on https://github.com/davinkevin/Podcast-Server/blob/v1.0.0/src/main/java/lan/dk/podcastserver/service/MultiPartFileSenderService.java
|
||||
*
|
||||
* @author Tom Desair (tom dot desair at atmire dot com)
|
||||
* @author Frederic Van Reet (frederic dot vanreet at atmire dot com)
|
||||
*
|
||||
*/
|
||||
public class MultipartFileSender {
|
||||
|
||||
protected final Logger log = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
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 int bufferSize = 1000000;
|
||||
|
||||
private static final long DEFAULT_EXPIRE_TIME = 60L * 60L * 1000L;
|
||||
|
||||
//no-cache so request is always performed for logging
|
||||
private static final String CACHE_CONTROL_SETTING = "private,no-cache";
|
||||
|
||||
private BufferedInputStream inputStream;
|
||||
private HttpServletRequest request;
|
||||
private HttpServletResponse response;
|
||||
private String contentType;
|
||||
private String disposition;
|
||||
private long lastModified;
|
||||
private long length;
|
||||
private String fileName;
|
||||
private String checksum;
|
||||
|
||||
public MultipartFileSender(final InputStream inputStream) {
|
||||
//Convert to BufferedInputStream so we can re-read the stream
|
||||
this.inputStream = new BufferedInputStream(inputStream);
|
||||
}
|
||||
|
||||
|
||||
public static MultipartFileSender fromInputStream(InputStream inputStream) {
|
||||
return new MultipartFileSender(inputStream);
|
||||
}
|
||||
|
||||
public MultipartFileSender with(HttpServletRequest httpRequest) {
|
||||
request = httpRequest;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MultipartFileSender with(HttpServletResponse httpResponse) {
|
||||
response = httpResponse;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MultipartFileSender withLength(long length) {
|
||||
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;
|
||||
}
|
||||
|
||||
public MultipartFileSender withMimetype(String mimetype) {
|
||||
this.contentType = mimetype;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MultipartFileSender withLastModified(long lastModified) {
|
||||
this.lastModified = lastModified;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MultipartFileSender withBufferSize(int bufferSize) {
|
||||
if(bufferSize > 0) {
|
||||
this.bufferSize = bufferSize;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public void serveResource() throws IOException {
|
||||
|
||||
// Validate and process range -------------------------------------------------------------
|
||||
|
||||
// Prepare some variables. The full Range represents the complete file.
|
||||
Range full = getFullRange();
|
||||
List<Range> ranges = getRanges(full);
|
||||
|
||||
if (ranges == null) {
|
||||
//The supplied range values were invalid
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("Content-Type : {}", contentType);
|
||||
// Initialize response.
|
||||
response.reset();
|
||||
response.setBufferSize(bufferSize);
|
||||
response.setHeader(CONTENT_TYPE, contentType);
|
||||
response.setHeader(ACCEPT_RANGES, BYTES);
|
||||
response.setHeader(ETAG, checksum);
|
||||
response.setDateHeader(LAST_MODIFIED, lastModified);
|
||||
response.setDateHeader(EXPIRES, System.currentTimeMillis() + DEFAULT_EXPIRE_TIME);
|
||||
|
||||
//No-cache so that we can log every download
|
||||
response.setHeader(CACHE_CONTROL, CACHE_CONTROL_SETTING);
|
||||
|
||||
|
||||
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 (OutputStream output = response.getOutputStream()) {
|
||||
|
||||
|
||||
if (hasNoRanges(full, ranges)) {
|
||||
|
||||
// Return full file.
|
||||
log.debug("Return full file");
|
||||
response.setContentType(contentType);
|
||||
response.setHeader(CONTENT_LENGTH, String.valueOf(length));
|
||||
Range.copy(inputStream, output, length, 0, length, bufferSize);
|
||||
|
||||
} else if (ranges.size() == 1) {
|
||||
|
||||
// Return single part of file.
|
||||
Range r = ranges.get(0);
|
||||
log.debug("Return 1 part of file : from ({}) to ({})", r.start, r.end);
|
||||
response.setContentType(contentType);
|
||||
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.
|
||||
Range.copy(inputStream, output, length, r.start, r.length, bufferSize);
|
||||
|
||||
} else {
|
||||
|
||||
// Return multiple parts of file.
|
||||
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) {
|
||||
log.debug("Return multi part of file : from ({}) to ({})", r.start, r.end);
|
||||
// Add multipart boundary and header fields for every range.
|
||||
sos.println("--" + MULTIPART_BOUNDARY);
|
||||
sos.println(CONTENT_TYPE + ": " + contentType);
|
||||
sos.println(CONTENT_RANGE + ": " + String.format(BYTES_RANGE_FORMAT, r.start, r.end, r.total));
|
||||
|
||||
//Mark position of inputstream so we can return to it later
|
||||
inputStream.mark(0);
|
||||
// Copy single part range of multi part range.
|
||||
Range.copy(inputStream, output, length, r.start, r.length, bufferSize);
|
||||
inputStream.reset();
|
||||
|
||||
sos.println();
|
||||
}
|
||||
|
||||
// End with multipart boundary.
|
||||
sos.println("--" + MULTIPART_BOUNDARY + "--");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public boolean isValid() throws IOException {
|
||||
if (response == null || request == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (inputStream == null) {
|
||||
log.error("Input stream has no content");
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (StringUtils.isEmpty(fileName)) {
|
||||
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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 (nonNull(ifNoneMatch) && matches(ifNoneMatch, checksum)) {
|
||||
log.debug("If-None-Match header should contain \"*\" or ETag. If so, then return 304.");
|
||||
response.setHeader(ETAG, checksum); // Required in 304.
|
||||
response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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 (isNull(ifNoneMatch) && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) {
|
||||
log.debug("If-Modified-Since header should be greater than LastModified. If so, then return 304.");
|
||||
response.setHeader(ETAG, checksum); // Required in 304.
|
||||
response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate request headers for resume ----------------------------------------------------
|
||||
|
||||
// If-Match header should contain "*" or ETag. If not, then return 412.
|
||||
String ifMatch = request.getHeader(IF_MATCH);
|
||||
if (nonNull(ifMatch) && !matches(ifMatch, checksum)) {
|
||||
log.error("If-Match header should contain \"*\" or ETag. If not, then return 412.");
|
||||
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If-Unmodified-Since header should be greater than LastModified. If not, then return 412.
|
||||
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 false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isNoRangeRequest() throws IOException {
|
||||
Range full = getFullRange();
|
||||
List<Range> ranges = getRanges(full);
|
||||
|
||||
if(hasNoRanges(full, ranges)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasNoRanges(final Range full, final List<Range> ranges) {
|
||||
return ranges != null && (ranges.isEmpty() || ranges.get(0) == full);
|
||||
}
|
||||
|
||||
private Range getFullRange() {
|
||||
return new Range(0, length - 1, length);
|
||||
}
|
||||
|
||||
|
||||
private List<Range> getRanges(final Range fullRange) throws IOException {
|
||||
List<Range> ranges = new ArrayList<>();
|
||||
|
||||
// Validate and process Range and If-Range headers.
|
||||
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*)*$")) {
|
||||
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 null;
|
||||
}
|
||||
|
||||
String ifRange = request.getHeader(IF_RANGE);
|
||||
if (nonNull(ifRange) && !ifRange.equals(fileName)) {
|
||||
try {
|
||||
//Assume that the If-Range contains a date
|
||||
long ifRangeTime = request.getDateHeader(IF_RANGE); // Throws IAE if invalid.
|
||||
|
||||
if (ifRangeTime == -1 || ifRangeTime + 1000 <= lastModified) {
|
||||
//Our file has been updated, send the full range
|
||||
ranges.add(fullRange);
|
||||
}
|
||||
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
//Assume that the If-Range contains an ETag
|
||||
if (!matches(ifRange, checksum)) {
|
||||
//Our file has been updated, send the full range
|
||||
ranges.add(fullRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If any valid If-Range header, then process each part of byte range.
|
||||
if (ranges.isEmpty()) {
|
||||
log.debug("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).
|
||||
long start = Range.sublong(part, 0, part.indexOf("-"));
|
||||
long end = Range.sublong(part, part.indexOf("-") + 1, part.length());
|
||||
|
||||
if (start == -1) {
|
||||
start = length - end;
|
||||
end = length - 1;
|
||||
} else if (end == -1 || end > length - 1) {
|
||||
end = length - 1;
|
||||
}
|
||||
|
||||
// Check if Range is syntactically valid. If not, then return 416.
|
||||
if (start > end) {
|
||||
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.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Add range.
|
||||
ranges.add(new Range(start, end, length));
|
||||
}
|
||||
}
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
private static boolean isNullOrEmpty(String disposition) {
|
||||
return StringUtils.isBlank(disposition);
|
||||
}
|
||||
|
||||
|
||||
private static class Range {
|
||||
long start;
|
||||
long end;
|
||||
long length;
|
||||
long total;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
this.end = end;
|
||||
this.length = this.end - this.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;
|
||||
}
|
||||
|
||||
private static void copy(InputStream input, OutputStream output, long inputSize, long start, long length, int bufferSize) throws IOException {
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
int read;
|
||||
|
||||
if (inputSize == length) {
|
||||
// Write full range.
|
||||
while ((read = input.read(buffer)) > 0) {
|
||||
output.write(buffer, 0, read);
|
||||
output.flush();
|
||||
}
|
||||
} else {
|
||||
input.skip(start);
|
||||
long toRead = length;
|
||||
|
||||
while ((read = input.read(buffer)) > 0) {
|
||||
if ((toRead -= read) > 0) {
|
||||
output.write(buffer, 0, read);
|
||||
output.flush();
|
||||
} else {
|
||||
output.write(buffer, 0, (int) toRead + read);
|
||||
output.flush();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean accepts(String acceptHeader, String toAccept) {
|
||||
String[] acceptValues = acceptHeader.split("\\s*(,|;)\\s*");
|
||||
Arrays.sort(acceptValues);
|
||||
|
||||
return Arrays.binarySearch(acceptValues, toAccept) > -1
|
||||
|| Arrays.binarySearch(acceptValues, toAccept.replaceAll("/.*$", "/*")) > -1
|
||||
|| Arrays.binarySearch(acceptValues, "*/*") > -1;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@@ -117,7 +117,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
|
||||
|
||||
//The group we try to add to our token
|
||||
context.turnOffAuthorisationSystem();
|
||||
Group internalGroup = new GroupBuilder().createGroup(context)
|
||||
Group internalGroup = GroupBuilder.createGroup(context)
|
||||
.withName("Internal Group")
|
||||
.build();
|
||||
context.restoreAuthSystemState();
|
||||
|
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
package org.dspace.app.rest;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.head;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.CharEncoding;
|
||||
import org.apache.solr.client.solrj.SolrQuery;
|
||||
import org.apache.solr.client.solrj.SolrServerException;
|
||||
import org.apache.solr.client.solrj.response.QueryResponse;
|
||||
import org.dspace.app.rest.builder.BitstreamBuilder;
|
||||
import org.dspace.app.rest.builder.CollectionBuilder;
|
||||
import org.dspace.app.rest.builder.CommunityBuilder;
|
||||
import org.dspace.app.rest.builder.GroupBuilder;
|
||||
import org.dspace.app.rest.builder.ItemBuilder;
|
||||
import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
|
||||
import org.dspace.content.Bitstream;
|
||||
import org.dspace.content.Collection;
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.eperson.Group;
|
||||
import org.dspace.solr.MockSolrServer;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Integration test to test the /api/core/bitstreams/[id]/content endpoint
|
||||
*
|
||||
* @author Tom Desair (tom dot desair at atmire dot com)
|
||||
* @author Frederic Van Reet (frederic dot vanreet at atmire dot com)
|
||||
*/
|
||||
public class BitstreamContentRestControllerIT extends AbstractControllerIntegrationTest {
|
||||
|
||||
private MockSolrServer mockSolrServer;
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception {
|
||||
super.setUp();
|
||||
mockSolrServer = new MockSolrServer("statistics");
|
||||
mockSolrServer.getSolrServer().deleteByQuery("*:*");
|
||||
mockSolrServer.getSolrServer().commit();
|
||||
}
|
||||
|
||||
@After
|
||||
public void destroy() throws Exception {
|
||||
super.destroy();
|
||||
mockSolrServer.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retrieveFullBitstream() throws Exception {
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
//** GIVEN **
|
||||
//1. A community-collection structure with one parent community and one collections.
|
||||
parentCommunity = CommunityBuilder.createCommunity(context)
|
||||
.withName("Parent Community")
|
||||
.build();
|
||||
|
||||
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build();
|
||||
|
||||
//2. A public item with a bitstream
|
||||
String bitstreamContent = "0123456789";
|
||||
|
||||
try(InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
|
||||
|
||||
Item publicItem1 = ItemBuilder.createItem(context, col1)
|
||||
.withTitle("Public item 1")
|
||||
.withIssueDate("2017-10-17")
|
||||
.withAuthor("Smith, Donald").withAuthor("Doe, John")
|
||||
.build();
|
||||
|
||||
Bitstream bitstream = BitstreamBuilder
|
||||
.createBitstream(context, publicItem1, is)
|
||||
.withName("Test bitstream")
|
||||
.withDescription("This is a bitstream to test range requests")
|
||||
.withMimeType("text/plain")
|
||||
.build();
|
||||
|
||||
//** WHEN **
|
||||
//We download the bitstream
|
||||
getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content"))
|
||||
|
||||
//** THEN **
|
||||
.andExpect(status().isOk())
|
||||
|
||||
//The Content Length must match the full length
|
||||
.andExpect(header().longValue("Content-Length", bitstreamContent.getBytes().length))
|
||||
//The server should indicate we support Range requests
|
||||
.andExpect(header().string("Accept-Ranges", "bytes"))
|
||||
//The ETag has to be based on the checksum
|
||||
.andExpect(header().string("ETag", bitstream.getChecksum()))
|
||||
//We expect the content type to match the bitstream mime type
|
||||
.andExpect(content().contentType("text/plain"))
|
||||
//THe bytes of the content must match the original content
|
||||
.andExpect(content().bytes(bitstreamContent.getBytes()));
|
||||
|
||||
//A If-None-Match HEAD request on the ETag must tell is the bitstream is not modified
|
||||
getClient().perform(head("/api/core/bitstreams/" + bitstream.getID() + "/content")
|
||||
.header("If-None-Match", bitstream.getChecksum()))
|
||||
.andExpect(status().isNotModified());
|
||||
|
||||
//The download and head request should also be logged as a statistics record
|
||||
checkNumberOfStatsRecords(bitstream, 2);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retrieveRangeBitstream() throws Exception {
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
//** GIVEN **
|
||||
//1. A community-collection structure with one parent community and one collections.
|
||||
parentCommunity = CommunityBuilder.createCommunity(context)
|
||||
.withName("Parent Community")
|
||||
.build();
|
||||
|
||||
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build();
|
||||
|
||||
//2. A public item with a bitstream
|
||||
String bitstreamContent = "0123456789";
|
||||
|
||||
try(InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
|
||||
|
||||
Item publicItem1 = ItemBuilder.createItem(context, col1)
|
||||
.withTitle("Public item 1")
|
||||
.withIssueDate("2017-10-17")
|
||||
.withAuthor("Smith, Donald").withAuthor("Doe, John")
|
||||
.build();
|
||||
|
||||
Bitstream bitstream = BitstreamBuilder
|
||||
.createBitstream(context, publicItem1, is)
|
||||
.withName("Test bitstream")
|
||||
.withDescription("This is a bitstream to test range requests")
|
||||
.withMimeType("text/plain")
|
||||
.build();
|
||||
|
||||
//** WHEN **
|
||||
//We download only a specific byte range of the bitstream
|
||||
getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")
|
||||
.header("Range", "bytes=1-3"))
|
||||
|
||||
//** THEN **
|
||||
.andExpect(status().is(206))
|
||||
|
||||
//The Content Length must match the requested range
|
||||
.andExpect(header().longValue("Content-Length", 3))
|
||||
//The server should indicate we support Range requests
|
||||
.andExpect(header().string("Accept-Ranges", "bytes"))
|
||||
//The ETag has to be based on the checksum
|
||||
.andExpect(header().string("ETag", bitstream.getChecksum()))
|
||||
//The response should give us details about the range
|
||||
.andExpect(header().string("Content-Range", "bytes 1-3/10"))
|
||||
//We expect the content type to match the bitstream mime type
|
||||
.andExpect(content().contentType("text/plain"))
|
||||
//We only expect the bytes 1, 2 and 3
|
||||
.andExpect(content().bytes("123".getBytes()));
|
||||
|
||||
//** WHEN **
|
||||
//We download the rest of the range
|
||||
getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")
|
||||
.header("Range", "bytes=4-"))
|
||||
|
||||
//** THEN **
|
||||
.andExpect(status().is(206))
|
||||
|
||||
//The Content Length must match the requested range
|
||||
.andExpect(header().longValue("Content-Length", 6))
|
||||
//The server should indicate we support Range requests
|
||||
.andExpect(header().string("Accept-Ranges", "bytes"))
|
||||
//The ETag has to be based on the checksum
|
||||
.andExpect(header().string("ETag", bitstream.getChecksum()))
|
||||
//The response should give us details about the range
|
||||
.andExpect(header().string("Content-Range", "bytes 4-9/10"))
|
||||
//We expect the content type to match the bitstream mime type
|
||||
.andExpect(content().contentType("text/plain"))
|
||||
//We all remaining bytes, starting at byte 4
|
||||
.andExpect(content().bytes("456789".getBytes()));
|
||||
|
||||
//Check that NO statistics record was logged for the Range requests
|
||||
checkNumberOfStatsRecords(bitstream, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBitstreamNotFound() throws Exception {
|
||||
getClient().perform(get("/api/core/bitstreams/" + UUID.randomUUID() + "/content"))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmbargoedBitstream() throws Exception {
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
//** GIVEN **
|
||||
//1. A community-collection structure with one parent community and one collections.
|
||||
parentCommunity = CommunityBuilder.createCommunity(context)
|
||||
.withName("Parent Community")
|
||||
.build();
|
||||
|
||||
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build();
|
||||
|
||||
//2. A public item with an embargoed bitstream
|
||||
String bitstreamContent = "Embargoed!";
|
||||
|
||||
try(InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
|
||||
|
||||
Item publicItem1 = ItemBuilder.createItem(context, col1)
|
||||
.withTitle("Public item 1")
|
||||
.withIssueDate("2017-10-17")
|
||||
.withAuthor("Smith, Donald").withAuthor("Doe, John")
|
||||
.build();
|
||||
|
||||
Bitstream bitstream = BitstreamBuilder
|
||||
.createBitstream(context, publicItem1, is)
|
||||
.withName("Test Embargoed Bitstream")
|
||||
.withDescription("This bitstream is embargoed")
|
||||
.withMimeType("text/plain")
|
||||
.withEmbargoPeriod("6 months")
|
||||
.build();
|
||||
|
||||
//** WHEN **
|
||||
//We download the bitstream
|
||||
getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content"))
|
||||
|
||||
//** THEN **
|
||||
.andExpect(status().isUnauthorized());
|
||||
|
||||
//An unauthorized request should not log statistics
|
||||
checkNumberOfStatsRecords(bitstream, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrivateBitstream() throws Exception {
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
//** GIVEN **
|
||||
//1. A community-collection structure with one parent community and one collections.
|
||||
parentCommunity = CommunityBuilder.createCommunity(context)
|
||||
.withName("Parent Community")
|
||||
.build();
|
||||
|
||||
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build();
|
||||
|
||||
//2. A public item with a private bitstream
|
||||
Item publicItem1 = ItemBuilder.createItem(context, col1)
|
||||
.withTitle("Public item 1")
|
||||
.withIssueDate("2017-10-17")
|
||||
.withAuthor("Smith, Donald").withAuthor("Doe, John")
|
||||
.build();
|
||||
|
||||
Group internalGroup = GroupBuilder.createGroup(context)
|
||||
.withName("Internal Group")
|
||||
.build();
|
||||
|
||||
String bitstreamContent = "Private!";
|
||||
try(InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
|
||||
|
||||
Bitstream bitstream = BitstreamBuilder
|
||||
.createBitstream(context, publicItem1, is)
|
||||
.withName("Test Embargoed Bitstream")
|
||||
.withDescription("This bitstream is embargoed")
|
||||
.withMimeType("text/plain")
|
||||
.withReaderGroup(internalGroup)
|
||||
.build();
|
||||
|
||||
//** WHEN **
|
||||
//We download the bitstream
|
||||
getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content"))
|
||||
|
||||
//** THEN **
|
||||
.andExpect(status().isUnauthorized());
|
||||
|
||||
//An unauthorized request should not log statistics
|
||||
checkNumberOfStatsRecords(bitstream, 0);
|
||||
|
||||
} finally {
|
||||
//** CLEANUP **
|
||||
GroupBuilder.cleaner().delete(internalGroup);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkNumberOfStatsRecords(Bitstream bitstream, int expectedNumberOfStatsRecords) throws SolrServerException, IOException {
|
||||
mockSolrServer.getSolrServer().commit();
|
||||
|
||||
SolrQuery query = new SolrQuery("id:\"" + bitstream.getID() + "\"")
|
||||
.setRows(0)
|
||||
.setStart(0);
|
||||
QueryResponse queryResponse = mockSolrServer.getSolrServer().query(query);
|
||||
assertEquals(expectedNumberOfStatsRecords, queryResponse.getResults().getNumFound());
|
||||
}
|
||||
|
||||
}
|
@@ -6,9 +6,17 @@
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
package org.dspace.app.rest;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import org.dspace.app.rest.builder.CollectionBuilder;
|
||||
import org.dspace.app.rest.builder.CommunityBuilder;
|
||||
@@ -28,6 +36,9 @@ import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
|
||||
/**
|
||||
* Integration test to test the /api/discover/browses endpoint
|
||||
* (Class has to start or end with IT to be picked up by the failsafe plugin)
|
||||
*
|
||||
* @author Tom Desair (tom dot desair at atmire dot com)
|
||||
* @author Raf Ponsaerts (raf dot ponsaerts at atmire dot com)
|
||||
*/
|
||||
public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTest {
|
||||
|
||||
@@ -121,31 +132,31 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe
|
||||
|
||||
//** GIVEN **
|
||||
//1. A community-collection structure with one parent community with sub-community and two collections.
|
||||
parentCommunity = new CommunityBuilder().createCommunity(context)
|
||||
parentCommunity = CommunityBuilder.createCommunity(context)
|
||||
.withName("Parent Community")
|
||||
.build();
|
||||
Community child1 = new CommunityBuilder().createSubCommunity(context, parentCommunity)
|
||||
Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity)
|
||||
.withName("Sub Community")
|
||||
.build();
|
||||
Collection col1 = new CollectionBuilder().createCollection(context, child1).withName("Collection 1").build();
|
||||
Collection col2 = new CollectionBuilder().createCollection(context, child1).withName("Collection 2").build();
|
||||
Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build();
|
||||
Collection col2 = CollectionBuilder.createCollection(context, child1).withName("Collection 2").build();
|
||||
|
||||
//2. Three public items that are readable by Anonymous with different subjects
|
||||
Item publicItem1 = new ItemBuilder().createItem(context, col1)
|
||||
Item publicItem1 = ItemBuilder.createItem(context, col1)
|
||||
.withTitle("Public item 1")
|
||||
.withIssueDate("2017-10-17")
|
||||
.withAuthor("Smith, Donald").withAuthor("Doe, John")
|
||||
.withSubject("ExtraEntry")
|
||||
.build();
|
||||
|
||||
Item publicItem2 = new ItemBuilder().createItem(context, col2)
|
||||
Item publicItem2 = ItemBuilder.createItem(context, col2)
|
||||
.withTitle("Public item 2")
|
||||
.withIssueDate("2016-02-13")
|
||||
.withAuthor("Smith, Maria").withAuthor("Doe, Jane")
|
||||
.withSubject("TestingForMore").withSubject("ExtraEntry")
|
||||
.build();
|
||||
|
||||
Item publicItem3 = new ItemBuilder().createItem(context, col2)
|
||||
Item publicItem3 = ItemBuilder.createItem(context, col2)
|
||||
.withTitle("Public item 2")
|
||||
.withIssueDate("2016-02-13")
|
||||
.withAuthor("Smith, Maria").withAuthor("Doe, Jane")
|
||||
@@ -200,32 +211,32 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe
|
||||
|
||||
//** GIVEN **
|
||||
//1. A community-collection structure with one parent community with sub-community and two collections.
|
||||
parentCommunity = new CommunityBuilder().createCommunity(context)
|
||||
parentCommunity = CommunityBuilder.createCommunity(context)
|
||||
.withName("Parent Community")
|
||||
.build();
|
||||
Community child1 = new CommunityBuilder().createSubCommunity(context, parentCommunity)
|
||||
Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity)
|
||||
.withName("Sub Community")
|
||||
.build();
|
||||
Collection col1 = new CollectionBuilder().createCollection(context, child1).withName("Collection 1").build();
|
||||
Collection col2 = new CollectionBuilder().createCollection(context, child1).withName("Collection 2").build();
|
||||
Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build();
|
||||
Collection col2 = CollectionBuilder.createCollection(context, child1).withName("Collection 2").build();
|
||||
|
||||
//2. Two public items with the same subject and another public item that contains that same subject, but also another one
|
||||
// All of the items are readable by an Anonymous user
|
||||
Item publicItem1 = new ItemBuilder().createItem(context, col1)
|
||||
Item publicItem1 = ItemBuilder.createItem(context, col1)
|
||||
.withTitle("zPublic item more")
|
||||
.withIssueDate("2017-10-17")
|
||||
.withAuthor("Smith, Donald").withAuthor("Doe, John")
|
||||
.withSubject("ExtraEntry").withSubject("AnotherTest")
|
||||
.build();
|
||||
|
||||
Item publicItem2 = new ItemBuilder().createItem(context, col2)
|
||||
Item publicItem2 = ItemBuilder.createItem(context, col2)
|
||||
.withTitle("Public item 2")
|
||||
.withIssueDate("2016-02-13")
|
||||
.withAuthor("Smith, Maria").withAuthor("Doe, Jane")
|
||||
.withSubject("AnotherTest")
|
||||
.build();
|
||||
|
||||
Item publicItem3 = new ItemBuilder().createItem(context, col2)
|
||||
Item publicItem3 = ItemBuilder.createItem(context, col2)
|
||||
.withTitle("Public item 3")
|
||||
.withIssueDate("2016-02-14")
|
||||
.withAuthor("Smith, Maria").withAuthor("Doe, Jane")
|
||||
@@ -274,24 +285,24 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe
|
||||
|
||||
//** GIVEN **
|
||||
//1. A community-collection structure with one parent community with sub-community and two collections.
|
||||
parentCommunity = new CommunityBuilder().createCommunity(context)
|
||||
parentCommunity = CommunityBuilder.createCommunity(context)
|
||||
.withName("Parent Community")
|
||||
.build();
|
||||
Community child1 = new CommunityBuilder().createSubCommunity(context, parentCommunity)
|
||||
Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity)
|
||||
.withName("Sub Community")
|
||||
.build();
|
||||
Collection col1 = new CollectionBuilder().createCollection(context, child1).withName("Collection 1").build();
|
||||
Collection col2 = new CollectionBuilder().createCollection(context, child1).withName("Collection 2").build();
|
||||
Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build();
|
||||
Collection col2 = CollectionBuilder.createCollection(context, child1).withName("Collection 2").build();
|
||||
|
||||
//2. Two public items that are readable by Anonymous
|
||||
Item publicItem1 = new ItemBuilder().createItem(context, col1)
|
||||
Item publicItem1 = ItemBuilder.createItem(context, col1)
|
||||
.withTitle("Public item 1")
|
||||
.withIssueDate("2017-10-17")
|
||||
.withAuthor("Smith, Donald").withAuthor("Doe, John")
|
||||
.withSubject("Java").withSubject("Unit Testing")
|
||||
.build();
|
||||
|
||||
Item publicItem2 = new ItemBuilder().createItem(context, col2)
|
||||
Item publicItem2 = ItemBuilder.createItem(context, col2)
|
||||
.withTitle("Public item 2")
|
||||
.withIssueDate("2016-02-13")
|
||||
.withAuthor("Smith, Maria").withAuthor("Doe, Jane")
|
||||
@@ -299,7 +310,7 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe
|
||||
.build();
|
||||
|
||||
//3. An item that has been made private
|
||||
Item privateItem = new ItemBuilder().createItem(context, col1)
|
||||
Item privateItem = ItemBuilder.createItem(context, col1)
|
||||
.withTitle("This is a private item")
|
||||
.withIssueDate("2015-03-12")
|
||||
.withAuthor("Duck, Donald")
|
||||
@@ -308,7 +319,7 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe
|
||||
.build();
|
||||
|
||||
//4. An item with an item-level embargo
|
||||
Item embargoedItem = new ItemBuilder().createItem(context, col2)
|
||||
Item embargoedItem = ItemBuilder.createItem(context, col2)
|
||||
.withTitle("An embargoed publication")
|
||||
.withIssueDate("2017-08-10")
|
||||
.withAuthor("Mouse, Mickey")
|
||||
@@ -317,11 +328,11 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe
|
||||
.build();
|
||||
|
||||
//5. An item that is only readable for an internal groups
|
||||
Group internalGroup = new GroupBuilder().createGroup(context)
|
||||
Group internalGroup = GroupBuilder.createGroup(context)
|
||||
.withName("Internal Group")
|
||||
.build();
|
||||
|
||||
Item internalItem = new ItemBuilder().createItem(context, col2)
|
||||
Item internalItem = ItemBuilder.createItem(context, col2)
|
||||
.withTitle("Internal publication")
|
||||
.withIssueDate("2016-09-19")
|
||||
.withAuthor("Doe, John")
|
||||
@@ -368,7 +379,7 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe
|
||||
;
|
||||
|
||||
//** CLEANUP **
|
||||
new GroupBuilder().delete(internalGroup);
|
||||
GroupBuilder.cleaner().delete(internalGroup);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -377,47 +388,47 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe
|
||||
|
||||
//** GIVEN **
|
||||
//1. A community-collection structure with one parent community with sub-community and two collections.
|
||||
parentCommunity = new CommunityBuilder().createCommunity(context)
|
||||
parentCommunity = CommunityBuilder.createCommunity(context)
|
||||
.withName("Parent Community")
|
||||
.build();
|
||||
Community child1 = new CommunityBuilder().createSubCommunity(context, parentCommunity)
|
||||
Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity)
|
||||
.withName("Sub Community")
|
||||
.build();
|
||||
Collection col1 = new CollectionBuilder().createCollection(context, child1).withName("Collection 1").build();
|
||||
Collection col2 = new CollectionBuilder().createCollection(context, child1).withName("Collection 2").build();
|
||||
Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build();
|
||||
Collection col2 = CollectionBuilder.createCollection(context, child1).withName("Collection 2").build();
|
||||
|
||||
//2. 7 public items that are readable by Anonymous
|
||||
Item item1 = new ItemBuilder().createItem(context, col1)
|
||||
Item item1 = ItemBuilder.createItem(context, col1)
|
||||
.withTitle("Item 1")
|
||||
.withIssueDate("2017-10-17")
|
||||
.build();
|
||||
|
||||
Item item2 = new ItemBuilder().createItem(context, col2)
|
||||
Item item2 = ItemBuilder.createItem(context, col2)
|
||||
.withTitle("Item 2")
|
||||
.withIssueDate("2016-02-13")
|
||||
.build();
|
||||
|
||||
Item item3 = new ItemBuilder().createItem(context, col1)
|
||||
Item item3 = ItemBuilder.createItem(context, col1)
|
||||
.withTitle("Item 3")
|
||||
.withIssueDate("2016-02-12")
|
||||
.build();
|
||||
|
||||
Item item4 = new ItemBuilder().createItem(context, col2)
|
||||
Item item4 = ItemBuilder.createItem(context, col2)
|
||||
.withTitle("Item 4")
|
||||
.withIssueDate("2016-02-11")
|
||||
.build();
|
||||
|
||||
Item item5 = new ItemBuilder().createItem(context, col1)
|
||||
Item item5 = ItemBuilder.createItem(context, col1)
|
||||
.withTitle("Item 5")
|
||||
.withIssueDate("2016-02-10")
|
||||
.build();
|
||||
|
||||
Item item6 = new ItemBuilder().createItem(context, col2)
|
||||
Item item6 = ItemBuilder.createItem(context, col2)
|
||||
.withTitle("Item 6")
|
||||
.withIssueDate("2016-01-13")
|
||||
.build();
|
||||
|
||||
Item item7 = new ItemBuilder().createItem(context, col1)
|
||||
Item item7 = ItemBuilder.createItem(context, col1)
|
||||
.withTitle("Item 7")
|
||||
.withIssueDate("2016-01-12")
|
||||
.build();
|
||||
|
@@ -16,6 +16,9 @@ import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Integration test for the {@link RootRestResourceController}
|
||||
*
|
||||
* @author Tom Desair (tom dot desair at atmire dot com)
|
||||
* @author Raf Ponsaerts (raf dot ponsaerts at atmire dot com)
|
||||
*/
|
||||
public class RootRestResourceControllerIT extends AbstractControllerIntegrationTest {
|
||||
|
||||
|
@@ -7,10 +7,6 @@
|
||||
*/
|
||||
package org.dspace.app.rest.builder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Date;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.authorize.ResourcePolicy;
|
||||
@@ -20,14 +16,7 @@ import org.dspace.authorize.service.ResourcePolicyService;
|
||||
import org.dspace.content.DSpaceObject;
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.content.factory.ContentServiceFactory;
|
||||
import org.dspace.content.service.BitstreamService;
|
||||
import org.dspace.content.service.BundleService;
|
||||
import org.dspace.content.service.CollectionService;
|
||||
import org.dspace.content.service.CommunityService;
|
||||
import org.dspace.content.service.DSpaceObjectService;
|
||||
import org.dspace.content.service.InstallItemService;
|
||||
import org.dspace.content.service.ItemService;
|
||||
import org.dspace.content.service.WorkspaceItemService;
|
||||
import org.dspace.content.service.*;
|
||||
import org.dspace.core.Constants;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.discovery.IndexingService;
|
||||
@@ -41,8 +30,15 @@ import org.joda.time.DateTimeZone;
|
||||
import org.joda.time.MutablePeriod;
|
||||
import org.joda.time.format.PeriodFormat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Abstract builder to construct DSpace Objects
|
||||
*
|
||||
* @author Tom Desair (tom dot desair at atmire dot com)
|
||||
* @author Raf Ponsaerts (raf dot ponsaerts at atmire dot com)
|
||||
*/
|
||||
public abstract class AbstractBuilder<T extends DSpaceObject> {
|
||||
|
||||
@@ -55,6 +51,7 @@ public abstract class AbstractBuilder<T extends DSpaceObject> {
|
||||
static GroupService groupService;
|
||||
static BundleService bundleService;
|
||||
static BitstreamService bitstreamService;
|
||||
static BitstreamFormatService bitstreamFormatService;
|
||||
static AuthorizeService authorizeService;
|
||||
static ResourcePolicyService resourcePolicyService;
|
||||
static IndexingService indexingService;
|
||||
@@ -74,6 +71,7 @@ public abstract class AbstractBuilder<T extends DSpaceObject> {
|
||||
groupService = EPersonServiceFactory.getInstance().getGroupService();
|
||||
bundleService = ContentServiceFactory.getInstance().getBundleService();
|
||||
bitstreamService = ContentServiceFactory.getInstance().getBitstreamService();
|
||||
bitstreamFormatService = ContentServiceFactory.getInstance().getBitstreamFormatService();
|
||||
authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService();
|
||||
resourcePolicyService = AuthorizeServiceFactory.getInstance().getResourcePolicyService();
|
||||
indexingService = DSpaceServicesFactory.getInstance().getServiceManager().getServiceByName(IndexingService.class.getName(),IndexingService.class);
|
||||
|
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
package org.dspace.app.rest.builder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.content.Bitstream;
|
||||
import org.dspace.content.BitstreamFormat;
|
||||
import org.dspace.content.Bundle;
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.content.service.DSpaceObjectService;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.eperson.Group;
|
||||
|
||||
/**
|
||||
* Builder class to build bitstreams in test cases
|
||||
*
|
||||
* @author Tom Desair (tom dot desair at atmire dot com)
|
||||
* @author Raf Ponsaerts (raf dot ponsaerts at atmire dot com)
|
||||
*/
|
||||
public class BitstreamBuilder extends AbstractBuilder<Bitstream>{
|
||||
|
||||
public static final String ORIGINAL = "ORIGINAL";
|
||||
|
||||
private Bitstream bitstream;
|
||||
private Item item;
|
||||
private Group readerGroup;
|
||||
|
||||
protected BitstreamBuilder() {
|
||||
|
||||
}
|
||||
|
||||
public static BitstreamBuilder createBitstream(Context context, Item item, InputStream is) throws SQLException, AuthorizeException, IOException {
|
||||
BitstreamBuilder builder = new BitstreamBuilder();
|
||||
return builder.create(context, item, is);
|
||||
}
|
||||
|
||||
private BitstreamBuilder create(Context context, Item item, InputStream is) throws SQLException, AuthorizeException, IOException {
|
||||
this.context = context;
|
||||
this.item = item;
|
||||
|
||||
Bundle originalBundle = getOriginalBundle(item);
|
||||
|
||||
bitstream = bitstreamService.create(context, originalBundle, is);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public BitstreamBuilder withName(String name) throws SQLException {
|
||||
bitstream.setName(context, name);
|
||||
return this;
|
||||
}
|
||||
|
||||
public BitstreamBuilder withDescription(String description) throws SQLException {
|
||||
bitstream.setDescription(context, description);
|
||||
return this;
|
||||
}
|
||||
|
||||
public BitstreamBuilder withMimeType(String mimeType) throws SQLException {
|
||||
BitstreamFormat bf = bitstreamFormatService
|
||||
.findByMIMEType(context, mimeType);
|
||||
|
||||
if (bf != null) {
|
||||
bitstream.setFormat(context, bf);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private Bundle getOriginalBundle(Item item) throws SQLException, AuthorizeException {
|
||||
List<Bundle> bundles = itemService.getBundles(item, ORIGINAL);
|
||||
Bundle targetBundle = null;
|
||||
|
||||
if( bundles.size() < 1 )
|
||||
{
|
||||
// not found, create a new one
|
||||
targetBundle = bundleService.create(context, item, ORIGINAL);
|
||||
}
|
||||
else
|
||||
{
|
||||
// put bitstreams into first bundle
|
||||
targetBundle = bundles.iterator().next();
|
||||
}
|
||||
|
||||
return targetBundle;
|
||||
}
|
||||
|
||||
public BitstreamBuilder withEmbargoPeriod(String embargoPeriod) {
|
||||
return setEmbargo(embargoPeriod, bitstream);
|
||||
}
|
||||
|
||||
public BitstreamBuilder withReaderGroup(Group group) {
|
||||
readerGroup = group;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Bitstream build() {
|
||||
try {
|
||||
bitstreamService.update(context, bitstream);
|
||||
itemService.update(context, item);
|
||||
|
||||
//Check if we need to make this bitstream private.
|
||||
if(readerGroup != null) {
|
||||
setOnlyReadPermission(bitstream, readerGroup, null);
|
||||
}
|
||||
|
||||
context.dispatchEvents();
|
||||
|
||||
indexingService.commit();
|
||||
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bitstream;
|
||||
}
|
||||
|
||||
protected DSpaceObjectService<Bitstream> getDsoService() {
|
||||
return bitstreamService;
|
||||
}
|
||||
}
|
@@ -7,24 +7,32 @@
|
||||
*/
|
||||
package org.dspace.app.rest.builder;
|
||||
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.content.Collection;
|
||||
import org.dspace.content.Community;
|
||||
import org.dspace.content.MetadataSchema;
|
||||
import org.dspace.content.service.DSpaceObjectService;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.discovery.SearchServiceException;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Builder to construct Collection objects
|
||||
*
|
||||
* @author Tom Desair (tom dot desair at atmire dot com)
|
||||
* @author Raf Ponsaerts (raf dot ponsaerts at atmire dot com)
|
||||
*/
|
||||
public class CollectionBuilder extends AbstractBuilder<Collection> {
|
||||
|
||||
private Collection collection;
|
||||
|
||||
public CollectionBuilder createCollection(final Context context, final Community parent) {
|
||||
protected CollectionBuilder() {
|
||||
|
||||
}
|
||||
|
||||
public static CollectionBuilder createCollection(final Context context, final Community parent) {
|
||||
CollectionBuilder builder = new CollectionBuilder();
|
||||
return builder.create(context, parent);
|
||||
}
|
||||
|
||||
private CollectionBuilder create(final Context context, final Community parent) {
|
||||
this.context = context;
|
||||
try {
|
||||
this.collection = collectionService.create(context, parent);
|
||||
|
@@ -11,20 +11,36 @@ import org.dspace.content.Community;
|
||||
import org.dspace.content.MetadataSchema;
|
||||
import org.dspace.content.service.DSpaceObjectService;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.discovery.SearchServiceException;
|
||||
|
||||
/**
|
||||
* Builder to construct Community objects
|
||||
*
|
||||
* @author Tom Desair (tom dot desair at atmire dot com)
|
||||
* @author Raf Ponsaerts (raf dot ponsaerts at atmire dot com)
|
||||
*/
|
||||
public class CommunityBuilder extends AbstractBuilder<Community> {
|
||||
|
||||
private Community community;
|
||||
|
||||
public CommunityBuilder createCommunity(final Context context) {
|
||||
protected CommunityBuilder() {
|
||||
|
||||
}
|
||||
|
||||
public static CommunityBuilder createCommunity(final Context context) {
|
||||
CommunityBuilder builder = new CommunityBuilder();
|
||||
return builder.create(context);
|
||||
}
|
||||
|
||||
private CommunityBuilder create(final Context context) {
|
||||
return createSubCommunity(context, null);
|
||||
}
|
||||
|
||||
public CommunityBuilder createSubCommunity(final Context context, final Community parent) {
|
||||
public static CommunityBuilder createSubCommunity(final Context context, final Community parent) {
|
||||
CommunityBuilder builder = new CommunityBuilder();
|
||||
return builder.createSub(context, parent);
|
||||
}
|
||||
|
||||
private CommunityBuilder createSub(final Context context, final Community parent) {
|
||||
this.context = context;
|
||||
try {
|
||||
community = communityService.create(parent, context);
|
||||
|
@@ -14,11 +14,33 @@ import org.dspace.eperson.Group;
|
||||
|
||||
/**
|
||||
* Builder to construct Group objects
|
||||
*
|
||||
* @author Tom Desair (tom dot desair at atmire dot com)
|
||||
* @author Raf Ponsaerts (raf dot ponsaerts at atmire dot com)
|
||||
*/
|
||||
public class GroupBuilder extends AbstractBuilder<Group> {
|
||||
|
||||
private Group group;
|
||||
|
||||
protected GroupBuilder() {
|
||||
|
||||
}
|
||||
|
||||
public static GroupBuilder createGroup(final Context context) {
|
||||
GroupBuilder builder = new GroupBuilder();
|
||||
return builder.create(context);
|
||||
}
|
||||
|
||||
private GroupBuilder create(final Context context) {
|
||||
this.context = context;
|
||||
try {
|
||||
group = groupService.create(context);
|
||||
} catch (Exception e) {
|
||||
return handleException(e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DSpaceObjectService<Group> getDsoService() {
|
||||
return groupService;
|
||||
@@ -29,16 +51,6 @@ public class GroupBuilder extends AbstractBuilder<Group> {
|
||||
return group;
|
||||
}
|
||||
|
||||
public GroupBuilder createGroup(final Context context) {
|
||||
this.context = context;
|
||||
try {
|
||||
group = groupService.create(context);
|
||||
} catch (Exception e) {
|
||||
return handleException(e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public GroupBuilder withName(String groupName) {
|
||||
try {
|
||||
groupService.setName(group, groupName);
|
||||
@@ -65,4 +77,9 @@ public class GroupBuilder extends AbstractBuilder<Group> {
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public static AbstractBuilder<Group> cleaner() {
|
||||
return new GroupBuilder();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -7,24 +7,40 @@
|
||||
*/
|
||||
package org.dspace.app.rest.builder;
|
||||
|
||||
import org.dspace.content.*;
|
||||
import org.dspace.content.Collection;
|
||||
import org.dspace.content.DCDate;
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.content.MetadataSchema;
|
||||
import org.dspace.content.WorkspaceItem;
|
||||
import org.dspace.content.service.DSpaceObjectService;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.eperson.Group;
|
||||
|
||||
/**
|
||||
* Builder to construct Item objects
|
||||
*
|
||||
* @author Tom Desair (tom dot desair at atmire dot com)
|
||||
* @author Raf Ponsaerts (raf dot ponsaerts at atmire dot com)
|
||||
*/
|
||||
public class ItemBuilder extends AbstractBuilder<Item> {
|
||||
|
||||
private WorkspaceItem workspaceItem;
|
||||
private Group readerGroup = null;
|
||||
|
||||
public ItemBuilder createItem(final Context context, final Collection col1) {
|
||||
protected ItemBuilder() {
|
||||
|
||||
}
|
||||
|
||||
public static ItemBuilder createItem(final Context context, final Collection col) {
|
||||
ItemBuilder builder = new ItemBuilder();
|
||||
return builder.create(context, col);
|
||||
}
|
||||
|
||||
private ItemBuilder create(final Context context, final Collection col) {
|
||||
this.context = context;
|
||||
|
||||
try {
|
||||
workspaceItem = workspaceItemService.create(context, col1, false);
|
||||
workspaceItem = workspaceItemService.create(context, col, false);
|
||||
} catch (Exception e) {
|
||||
return handleException(e);
|
||||
}
|
||||
|
@@ -7,12 +7,20 @@
|
||||
*/
|
||||
package org.dspace.app.rest.matcher;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
|
||||
import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
|
||||
import static org.dspace.app.rest.test.AbstractControllerIntegrationTest.REST_SERVER_URL;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
|
||||
/**
|
||||
* Class to match JSON browse entries in ITs
|
||||
*
|
||||
* @author Tom Desair (tom dot desair at atmire dot com)
|
||||
* @author Raf Ponsaerts (raf dot ponsaerts at atmire dot com)
|
||||
*/
|
||||
public class BrowseEntryResourceMatcher {
|
||||
public static Matcher<? super Object> matchBrowseEntry(String value, int expectedCount) {
|
||||
return allOf(
|
||||
|
@@ -9,7 +9,10 @@ package org.dspace.app.rest.matcher;
|
||||
|
||||
import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
|
||||
import static org.dspace.app.rest.test.AbstractControllerIntegrationTest.REST_SERVER_URL;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.text.IsEqualIgnoringCase.equalToIgnoringCase;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
@@ -17,6 +20,9 @@ import org.hamcrest.Matchers;
|
||||
|
||||
/**
|
||||
* Utility class to construct a Matcher for a browse index
|
||||
*
|
||||
* @author Tom Desair (tom dot desair at atmire dot com)
|
||||
* @author Raf Ponsaerts (raf dot ponsaerts at atmire dot com)
|
||||
*/
|
||||
public class BrowseIndexMatcher {
|
||||
|
||||
|
@@ -20,6 +20,9 @@ import org.hamcrest.Matcher;
|
||||
|
||||
/**
|
||||
* Utility class to construct a Matcher for an item
|
||||
*
|
||||
* @author Tom Desair (tom dot desair at atmire dot com)
|
||||
* @author Raf Ponsaerts (raf dot ponsaerts at atmire dot com)
|
||||
*/
|
||||
public class ItemMatcher {
|
||||
|
||||
|
@@ -26,6 +26,7 @@ import org.junit.Assert;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.web.support.ErrorPageFilter;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
@@ -46,6 +47,8 @@ import org.springframework.web.context.WebApplicationContext;
|
||||
/**
|
||||
* Abstract controller integration test class that will take care of setting up the
|
||||
* environment to run the integration test
|
||||
*
|
||||
* @author Tom Desair (tom dot desair at atmire dot com)
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringBootTest(classes = {Application.class, ApplicationConfig.class, WebSecurityConfiguration.class})
|
||||
@@ -92,9 +95,10 @@ public class AbstractControllerIntegrationTest extends AbstractIntegrationTestWi
|
||||
}
|
||||
|
||||
DefaultMockMvcBuilder mockMvcBuilder = webAppContextSetup(webApplicationContext)
|
||||
//Always log the repsonse to debug
|
||||
//Always log the response to debug
|
||||
.alwaysDo(MockMvcResultHandlers.log())
|
||||
//Add all filter implementations
|
||||
.addFilters(new ErrorPageFilter())
|
||||
.addFilters(requestFilters.toArray(new Filter[requestFilters.size()]));
|
||||
|
||||
if(StringUtils.isNotBlank(authToken)) {
|
||||
|
@@ -143,8 +143,7 @@ public class AbstractIntegrationTestWithDatabase extends AbstractDSpaceIntegrati
|
||||
* but no execution order is guaranteed
|
||||
*/
|
||||
@After
|
||||
public void destroy()
|
||||
{
|
||||
public void destroy() throws Exception {
|
||||
// Cleanup our global context object
|
||||
try {
|
||||
if(context == null || !context.isValid()){
|
||||
|
@@ -16,6 +16,8 @@ import org.springframework.test.context.ContextCustomizerFactory;
|
||||
|
||||
/**
|
||||
* Context customizer factory to set the parent context of our Spring Boot application in TEST mode
|
||||
*
|
||||
* @author Tom Desair (tom dot desair at atmire dot com)
|
||||
*/
|
||||
public class DSpaceKernelContextCustomizerFactory implements ContextCustomizerFactory {
|
||||
|
||||
|
@@ -0,0 +1,528 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
package org.dspace.app.rest.utils;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Date;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.CharEncoding;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper;
|
||||
|
||||
/**
|
||||
* Test class for MultipartFileSender
|
||||
*
|
||||
* @author Tom Desair (tom dot desair at atmire dot com)
|
||||
* @author Frederic Van Reet (frederic dot vanreet at atmire dot com)
|
||||
*/
|
||||
public class MultipartFileSenderTest {
|
||||
|
||||
/**
|
||||
* log4j category
|
||||
*/
|
||||
private static final Logger log = Logger.getLogger(MultipartFileSenderTest.class);
|
||||
|
||||
private InputStream is;
|
||||
private String mimeType;
|
||||
private long lastModified;
|
||||
private long length;
|
||||
private String fileName;
|
||||
private String checksum;
|
||||
|
||||
|
||||
private HttpServletRequest request;
|
||||
|
||||
private HttpServletResponse response;
|
||||
|
||||
private ContentCachingRequestWrapper requestWrapper;
|
||||
private ContentCachingResponseWrapper responseWrapper;
|
||||
|
||||
|
||||
/**
|
||||
* This method will be run before every test as per @Before. It will
|
||||
* initialize resources required for the tests.
|
||||
* <p>
|
||||
* Other methods can be annotated with @Before here or in subclasses
|
||||
* but no execution order is guaranteed
|
||||
*/
|
||||
@Before
|
||||
public void init() throws AuthorizeException {
|
||||
try {
|
||||
String content = "0123456789";
|
||||
|
||||
this.is = IOUtils.toInputStream(content, CharEncoding.UTF_8);
|
||||
this.fileName = "Test-Item.txt";
|
||||
this.mimeType = "text/plain";
|
||||
this.lastModified = new Date().getTime();
|
||||
this.length = content.getBytes().length;
|
||||
this.checksum = "testsum";
|
||||
|
||||
this.request = mock(HttpServletRequest.class);
|
||||
this.response = new MockHttpServletResponse();
|
||||
|
||||
//Using wrappers so we can save the content of the bodies and use them for tests
|
||||
this.requestWrapper = new ContentCachingRequestWrapper(request);
|
||||
this.responseWrapper = new ContentCachingResponseWrapper(response);
|
||||
} catch (IOException ex) {
|
||||
log.error("IO Error in init", ex);
|
||||
fail("SQL Error in init: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will be run after every test as per @After. It will
|
||||
* clean resources initialized by the @Before methods.
|
||||
* <p>
|
||||
* Other methods can be annotated with @After here or in subclasses
|
||||
* but no execution order is guaranteed
|
||||
*/
|
||||
@After
|
||||
public void destroy() {
|
||||
try {
|
||||
is.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test if Range header is supported and gives back the right range
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void testRangeHeader() throws Exception {
|
||||
MultipartFileSender multipartFileSender = MultipartFileSender
|
||||
.fromInputStream(is)
|
||||
.with(requestWrapper)
|
||||
.with(responseWrapper)
|
||||
.withFileName(fileName)
|
||||
.withChecksum(checksum)
|
||||
.withMimetype(mimeType)
|
||||
.withLength(length);
|
||||
|
||||
when(request.getHeader(eq("If-Range"))).thenReturn("not_file_to_serve.txt");
|
||||
when(request.getHeader(eq("Range"))).thenReturn("bytes=1-3");
|
||||
|
||||
multipartFileSender.serveResource();
|
||||
|
||||
String content = new String(responseWrapper.getContentAsByteArray(), CharEncoding.UTF_8);
|
||||
|
||||
assertEquals("123", content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if we can just request the full file without ranges
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void testFullFileReturn() throws Exception {
|
||||
MultipartFileSender multipartFileSender = MultipartFileSender
|
||||
.fromInputStream(is)
|
||||
.with(requestWrapper)
|
||||
.with(responseWrapper)
|
||||
.withFileName(fileName)
|
||||
.withChecksum(checksum)
|
||||
.withMimetype(mimeType)
|
||||
.withLength(length);
|
||||
|
||||
multipartFileSender.serveResource();
|
||||
|
||||
String content = new String(responseWrapper.getContentAsByteArray(), CharEncoding.UTF_8);
|
||||
|
||||
assertEquals("0123456789", content);
|
||||
assertEquals(checksum, responseWrapper.getHeader("ETag"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for support of Open ranges
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void testOpenRange() throws Exception {
|
||||
MultipartFileSender multipartFileSender = MultipartFileSender
|
||||
.fromInputStream(is)
|
||||
.with(requestWrapper)
|
||||
.with(responseWrapper)
|
||||
.withFileName(fileName)
|
||||
.withChecksum(checksum)
|
||||
.withMimetype(mimeType)
|
||||
.withLength(length);
|
||||
|
||||
when(request.getHeader(eq("Range"))).thenReturn("bytes=5-");
|
||||
|
||||
multipartFileSender.serveResource();
|
||||
|
||||
String content = new String(responseWrapper.getContentAsByteArray(), CharEncoding.UTF_8);
|
||||
|
||||
assertEquals("56789", content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test support for multiple ranges
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void testMultipleRanges() throws Exception {
|
||||
MultipartFileSender multipartFileSender = MultipartFileSender
|
||||
.fromInputStream(is)
|
||||
.with(requestWrapper)
|
||||
.with(responseWrapper)
|
||||
.withFileName(fileName)
|
||||
.withChecksum(checksum)
|
||||
.withMimetype(mimeType)
|
||||
.withLength(length);
|
||||
|
||||
when(request.getHeader(eq("Range"))).thenReturn("bytes=1-2,3-4,5-9");
|
||||
|
||||
multipartFileSender.serveResource();
|
||||
|
||||
String content = new String(responseWrapper.getContentAsByteArray(), CharEncoding.UTF_8);
|
||||
|
||||
assertEquals("--MULTIPART_BYTERANGES" +
|
||||
"Content-Type: text/plain" +
|
||||
"Content-Range: bytes 1-2/10" +
|
||||
"12" +
|
||||
"--MULTIPART_BYTERANGES" +
|
||||
"Content-Type: text/plain" +
|
||||
"Content-Range: bytes 3-4/10" +
|
||||
"34" +
|
||||
"--MULTIPART_BYTERANGES" +
|
||||
"Content-Type: text/plain" +
|
||||
"Content-Range: bytes 5-9/10" +
|
||||
"56789" +
|
||||
"--MULTIPART_BYTERANGES--".replace("\n", "").replace("\r", "")
|
||||
, content.replace("\n", "").replace("\r", "")
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test with a unvalid Range header, should return status 416
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void testInvalidRange() throws Exception {
|
||||
MultipartFileSender multipartFileSender = MultipartFileSender
|
||||
.fromInputStream(is)
|
||||
.with(requestWrapper)
|
||||
.with(responseWrapper)
|
||||
.withFileName(fileName)
|
||||
.withChecksum(checksum)
|
||||
.withMimetype(mimeType)
|
||||
.withLength(length);
|
||||
|
||||
when(request.getHeader(eq("Range"))).thenReturn("bytes=invalid");
|
||||
|
||||
multipartFileSender.serveResource();
|
||||
|
||||
assertEquals(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE, responseWrapper.getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the ETAG is in the response header
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void testEtagInResponse() throws Exception {
|
||||
MultipartFileSender multipartFileSender = MultipartFileSender
|
||||
.fromInputStream(is)
|
||||
.with(requestWrapper)
|
||||
.with(responseWrapper)
|
||||
.withFileName(fileName)
|
||||
.withChecksum(checksum)
|
||||
.withMimetype(mimeType)
|
||||
.withLength(length);
|
||||
|
||||
|
||||
when(request.getHeader(eq("Range"))).thenReturn("bytes=1-3");
|
||||
|
||||
multipartFileSender.serveResource();
|
||||
|
||||
String etag = responseWrapper.getHeader("Etag");
|
||||
|
||||
assertEquals(checksum, etag);
|
||||
}
|
||||
|
||||
//Check that a head request doesn't return any body, but returns the headers
|
||||
@Test
|
||||
public void testHeadRequest() throws Exception {
|
||||
MultipartFileSender multipartFileSender = MultipartFileSender
|
||||
.fromInputStream(is)
|
||||
.with(requestWrapper)
|
||||
.with(responseWrapper)
|
||||
.withFileName(fileName)
|
||||
.withChecksum(checksum)
|
||||
.withMimetype(mimeType)
|
||||
.withLength(length);
|
||||
|
||||
|
||||
when(request.getMethod()).thenReturn("HEAD");
|
||||
|
||||
multipartFileSender.serveResource();
|
||||
|
||||
String content = new String(responseWrapper.getContentAsByteArray(), CharEncoding.UTF_8);
|
||||
|
||||
assertEquals("bytes", responseWrapper.getHeader("Accept-Ranges"));
|
||||
assertEquals(checksum, responseWrapper.getHeader("ETag"));
|
||||
assertEquals("", content);
|
||||
assertEquals(200, responseWrapper.getStatusCode());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* If ETAG is equal to that of the requested Resource then this should return 304
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void testIfNoneMatchFail() throws Exception {
|
||||
MultipartFileSender multipartFileSender = MultipartFileSender
|
||||
.fromInputStream(is)
|
||||
.with(requestWrapper)
|
||||
.with(responseWrapper)
|
||||
.withFileName(fileName)
|
||||
.withChecksum(checksum)
|
||||
.withMimetype(mimeType)
|
||||
.withLength(length);
|
||||
|
||||
when(request.getHeader(eq("If-None-Match"))).thenReturn(checksum);
|
||||
|
||||
multipartFileSender.isValid();
|
||||
|
||||
assertEquals(HttpServletResponse.SC_NOT_MODIFIED, responseWrapper.getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Happy path of If-None-Match header
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void testIfNoneMatchPass() throws Exception {
|
||||
MultipartFileSender multipartFileSender = MultipartFileSender
|
||||
.fromInputStream(is)
|
||||
.with(requestWrapper)
|
||||
.with(responseWrapper)
|
||||
.withFileName(fileName)
|
||||
.withChecksum(checksum)
|
||||
.withMimetype(mimeType)
|
||||
.withLength(length);
|
||||
|
||||
|
||||
when(request.getHeader(eq("If-None-Match"))).thenReturn("pretendthisisarandomchecksumnotequaltotherequestedbitstream");
|
||||
|
||||
multipartFileSender.isValid();
|
||||
multipartFileSender.serveResource();
|
||||
|
||||
assertEquals(HttpServletResponse.SC_OK, responseWrapper.getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* If the bitstream has no filename this should throw an internal server error
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void testNoFileName() throws Exception {
|
||||
MultipartFileSender multipartFileSender = MultipartFileSender
|
||||
.fromInputStream(is)
|
||||
.with(requestWrapper)
|
||||
.with(responseWrapper)
|
||||
.withChecksum(checksum)
|
||||
.withMimetype(mimeType)
|
||||
.withLength(length);
|
||||
|
||||
|
||||
multipartFileSender.isValid();
|
||||
|
||||
|
||||
assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, responseWrapper.getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the Modified Since precondition works, should return 304 if it hasn't been modified
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void testIfModifiedSinceNotModifiedSince() throws Exception {
|
||||
Long time = new Date().getTime();
|
||||
MultipartFileSender multipartFileSender = MultipartFileSender
|
||||
.fromInputStream(is)
|
||||
.with(requestWrapper)
|
||||
.withFileName(fileName)
|
||||
.with(responseWrapper)
|
||||
.withChecksum(checksum)
|
||||
.withMimetype(mimeType)
|
||||
.withLength(length)
|
||||
.withLastModified(time);
|
||||
|
||||
when(request.getDateHeader(eq("If-Modified-Since"))).thenReturn(time + 100000);
|
||||
when(request.getDateHeader(eq("If-Unmodified-Since"))).thenReturn(-1L);
|
||||
|
||||
multipartFileSender.isValid();
|
||||
|
||||
assertEquals(HttpServletResponse.SC_NOT_MODIFIED, responseWrapper.getStatusCode());
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Happy path for modified since
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void testIfModifiedSinceModifiedSince() throws Exception {
|
||||
Long time = new Date().getTime();
|
||||
MultipartFileSender multipartFileSender = MultipartFileSender
|
||||
.fromInputStream(is)
|
||||
.with(requestWrapper)
|
||||
.withFileName(fileName)
|
||||
.with(responseWrapper)
|
||||
.withChecksum(checksum)
|
||||
.withMimetype(mimeType)
|
||||
.withLength(length)
|
||||
.withLastModified(time);
|
||||
|
||||
when(request.getDateHeader(eq("If-Modified-Since"))).thenReturn(time - 100000);
|
||||
when(request.getDateHeader(eq("If-Unmodified-Since"))).thenReturn(-1L);
|
||||
|
||||
multipartFileSender.isValid();
|
||||
multipartFileSender.serveResource();
|
||||
|
||||
assertEquals(HttpServletResponse.SC_OK, responseWrapper.getStatusCode());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* If the If-Match doesn't match the ETAG then return 416 Status code
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void testIfMatchNoMatch() throws Exception {
|
||||
Long time = new Date().getTime();
|
||||
MultipartFileSender multipartFileSender = MultipartFileSender
|
||||
.fromInputStream(is)
|
||||
.with(requestWrapper)
|
||||
.withFileName(fileName)
|
||||
.with(responseWrapper)
|
||||
.withChecksum(checksum)
|
||||
.withMimetype(mimeType)
|
||||
.withLength(length)
|
||||
.withLastModified(time);
|
||||
|
||||
when(request.getDateHeader(eq("If-Modified-Since"))).thenReturn(-1L);
|
||||
when(request.getDateHeader(eq("If-Unmodified-Since"))).thenReturn(-1L);
|
||||
when(request.getHeader(eq("If-Match"))).thenReturn("None-Matching-ETAG");
|
||||
|
||||
multipartFileSender.isValid();
|
||||
|
||||
assertEquals(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE, responseWrapper.getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* If matches then just return resource
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void testIfMatchMatch() throws Exception {
|
||||
Long time = new Date().getTime();
|
||||
MultipartFileSender multipartFileSender = MultipartFileSender
|
||||
.fromInputStream(is)
|
||||
.with(requestWrapper)
|
||||
.withFileName(fileName)
|
||||
.with(responseWrapper)
|
||||
.withChecksum(checksum)
|
||||
.withMimetype(mimeType)
|
||||
.withLength(length)
|
||||
.withLastModified(time);
|
||||
|
||||
when(request.getDateHeader(eq("If-Modified-Since"))).thenReturn(-1L);
|
||||
when(request.getDateHeader(eq("If-Unmodified-Since"))).thenReturn(-1L);
|
||||
when(request.getHeader(eq("If-Match"))).thenReturn(checksum);
|
||||
|
||||
multipartFileSender.isValid();
|
||||
|
||||
assertEquals(HttpServletResponse.SC_OK, responseWrapper.getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* If not modified since given date then return resource
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void testIfUnmodifiedSinceNotModifiedSince() throws Exception {
|
||||
Long time = new Date().getTime();
|
||||
MultipartFileSender multipartFileSender = MultipartFileSender
|
||||
.fromInputStream(is)
|
||||
.with(requestWrapper)
|
||||
.withFileName(fileName)
|
||||
.with(responseWrapper)
|
||||
.withChecksum(checksum)
|
||||
.withMimetype(mimeType)
|
||||
.withLength(length)
|
||||
.withLastModified(time);
|
||||
|
||||
when(request.getDateHeader(eq("If-Unmodified-Since"))).thenReturn(time + 100000);
|
||||
when(request.getDateHeader(eq("If-Modified-Since"))).thenReturn(-1L);
|
||||
|
||||
multipartFileSender.isValid();
|
||||
|
||||
assertEquals(HttpServletResponse.SC_OK, responseWrapper.getStatusCode());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* If modified since given date then return 412
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void testIfUnmodifiedSinceModifiedSince() throws Exception {
|
||||
Long time = new Date().getTime();
|
||||
MultipartFileSender multipartFileSender = MultipartFileSender
|
||||
.fromInputStream(is)
|
||||
.with(requestWrapper)
|
||||
.withFileName(fileName)
|
||||
.with(responseWrapper)
|
||||
.withChecksum(checksum)
|
||||
.withMimetype(mimeType)
|
||||
.withLength(length)
|
||||
.withLastModified(time);
|
||||
|
||||
when(request.getDateHeader(eq("If-Unmodified-Since"))).thenReturn(time - 100000);
|
||||
when(request.getDateHeader(eq("If-Modified-Since"))).thenReturn(-1L);
|
||||
|
||||
multipartFileSender.isValid();
|
||||
|
||||
assertEquals(HttpServletResponse.SC_PRECONDITION_FAILED, responseWrapper.getStatusCode());
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@@ -1813,7 +1813,7 @@ webui.suggest.enable = false
|
||||
# inside that snipet is your Google Analytics key usually found in this line:
|
||||
# _uacct = "UA-XXXXXXX-X"
|
||||
# Take this key (just the UA-XXXXXX-X part) and place it here in this parameter.
|
||||
# jspui.google.analytics.key=UA-XXXXXX-X
|
||||
# google.analytics.key=UA-XXXXXX-X
|
||||
|
||||
#---------------------------------------------------------------#
|
||||
#--------------XMLUI SPECIFIC CONFIGURATIONS--------------------#
|
||||
@@ -1919,15 +1919,6 @@ mirage2.item-view.bitstream.href.label.2 = title
|
||||
#xmlui.bitstream.mods = true
|
||||
#xmlui.bitstream.mets = true
|
||||
|
||||
# If you would like to use Google Analytics to track general website statistics then
|
||||
# use the following parameter to provide your Analytics key. First sign up for an
|
||||
# account at http://analytics.google.com, then create an entry for your repository
|
||||
# website. Analytics will give you a snipet of JavaScript code to place on your site,
|
||||
# inside that snipet is your Google Analytics key usually found in this line:
|
||||
# _uacct = "UA-XXXXXXX-X"
|
||||
# Take this key (just the UA-XXXXXX-X part) and place it here in this parameter.
|
||||
#xmlui.google.analytics.key=UA-XXXXXX-X
|
||||
|
||||
# Assign how many page views will be recorded and displayed in the control panel's
|
||||
# activity viewer. The activity tab allows an administrator to debug problems in a
|
||||
# running DSpace by understanding who and how their dspace is currently being used.
|
||||
|
@@ -7,5 +7,4 @@
|
||||
# The class names of the modules which the dspace servicemanager will attempt to retrieve.
|
||||
# These classes contain the paths to where to spring files are loaded
|
||||
spring.springloader.modules=org.dspace.app.configuration.APISpringLoader,\
|
||||
org.dspace.app.xmlui.configuration.XMLUISpringLoader,\
|
||||
org.dspace.app.webui.configuration.JSPUISpringLoader
|
||||
org.dspace.app.rest.configuration.RESTSpringLoader
|
||||
|
@@ -316,6 +316,33 @@
|
||||
<extension>qt</extension>
|
||||
</bitstream-type>
|
||||
|
||||
<bitstream-type>
|
||||
<mimetype>video/mp4</mimetype>
|
||||
<short_description>Video MP4</short_description>
|
||||
<description>Video MP4</description>
|
||||
<support_level>1</support_level>
|
||||
<internal>false</internal>
|
||||
<extension>mp4</extension>
|
||||
</bitstream-type>
|
||||
|
||||
<bitstream-type>
|
||||
<mimetype>video/ogg</mimetype>
|
||||
<short_description>Video OGG</short_description>
|
||||
<description>Video OGG</description>
|
||||
<support_level>1</support_level>
|
||||
<internal>false</internal>
|
||||
<extension>ogg</extension>
|
||||
</bitstream-type>
|
||||
|
||||
<bitstream-type>
|
||||
<mimetype>video/webm</mimetype>
|
||||
<short_description>Video WEBM</short_description>
|
||||
<description>Video WEBM</description>
|
||||
<support_level>1</support_level>
|
||||
<internal>false</internal>
|
||||
<extension>webm</extension>
|
||||
</bitstream-type>
|
||||
|
||||
<bitstream-type>
|
||||
<mimetype>audio/x-mpeg</mimetype>
|
||||
<short_description>MPEG Audio</short_description>
|
||||
@@ -736,4 +763,22 @@
|
||||
<extension>epub</extension>
|
||||
</bitstream-type>
|
||||
|
||||
<bitstream-type>
|
||||
<mimetype>video/mp4</mimetype>
|
||||
<short_description>mp4</short_description>
|
||||
<description>mpeg4</description>
|
||||
<support_level>1</support_level>
|
||||
<internal>false</internal>
|
||||
<extension>mp4</extension>
|
||||
</bitstream-type>
|
||||
|
||||
<bitstream-type>
|
||||
<mimetype>audio/mpeg</mimetype>
|
||||
<short_description>mp3</short_description>
|
||||
<description>MPEG audio</description>
|
||||
<support_level>1</support_level>
|
||||
<internal>false</internal>
|
||||
<extension>mp3</extension>
|
||||
</bitstream-type>
|
||||
|
||||
</dspace-bitstream-types>
|
||||
|
@@ -92,10 +92,6 @@
|
||||
|
||||
<bean class="org.dspace.license.CreativeCommonsServiceImpl"/>
|
||||
|
||||
<!-- Statistics services are both lazy loaded (by name), as you are likely just using ONE of them and not both -->
|
||||
<bean id="elasticSearchLoggerService" class="org.dspace.statistics.ElasticSearchLoggerServiceImpl" lazy-init="true"/>
|
||||
<bean id="solrLoggerService" class="org.dspace.statistics.SolrLoggerServiceImpl" lazy-init="true"/>
|
||||
|
||||
<bean id="spiderDetectorService" class="org.dspace.statistics.util.SpiderDetectorServiceImpl"/>
|
||||
|
||||
<bean class="org.dspace.versioning.VersionHistoryServiceImpl"/>
|
||||
|
@@ -9,24 +9,24 @@
|
||||
|
||||
-->
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:context="http://www.springframework.org/schema/context"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:context="http://www.springframework.org/schema/context"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans
|
||||
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
|
||||
http://www.springframework.org/schema/context
|
||||
http://www.springframework.org/schema/context/spring-context-2.5.xsd"
|
||||
default-autowire-candidates="*Service,*DAO,javax.sql.DataSource">
|
||||
default-autowire-candidates="*Service,*DAO,javax.sql.DataSource">
|
||||
|
||||
<context:annotation-config /> <!-- allows us to use spring annotations in beans -->
|
||||
<!-- NOTE: I am not convinced this is a good idea, it is really slow and I think possibly dangerous -AZ -->
|
||||
<!--
|
||||
<context:component-scan base-package="org.dspace" name-generator="org.dspace.servicemanager.spring.FullPathBeanNameGenerator" />
|
||||
-->
|
||||
|
||||
|
||||
<bean class="org.dspace.discovery.SolrServiceImpl" id="org.dspace.discovery.SearchService"/>
|
||||
|
||||
<alias name="org.dspace.discovery.SearchService" alias="org.dspace.discovery.IndexingService"/>
|
||||
|
||||
<!--<bean class="org.dspace.discovery.SolrServiceIndexOutputPlugin" id="solrServiceIndexOutputPlugin"/>-->
|
||||
|
||||
<!-- Statistics services are both lazy loaded (by name), as you are likely just using ONE of them and not both -->
|
||||
<bean id="elasticSearchLoggerService" class="org.dspace.statistics.ElasticSearchLoggerServiceImpl" lazy-init="true"/>
|
||||
<bean id="solrLoggerService" class="org.dspace.statistics.SolrLoggerServiceImpl" lazy-init="true"/>
|
||||
|
||||
</beans>
|
1
dspace/config/spring/rest/README.md
Normal file
1
dspace/config/spring/rest/README.md
Normal file
@@ -0,0 +1 @@
|
||||
You should only add Spring XML definition files here if there is really no way to load them through automatic component scanning.
|
24
dspace/config/spring/rest/event-service-listeners.xml
Normal file
24
dspace/config/spring/rest/event-service-listeners.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans
|
||||
xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:util="http://www.springframework.org/schema/util"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
|
||||
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.3.xsd">
|
||||
|
||||
<!-- Inject the Default LoggerUsageEventListener into the EventService -->
|
||||
<bean class="org.dspace.usage.LoggerUsageEventListener">
|
||||
<property name="eventService" ref="org.dspace.services.EventService"/>
|
||||
</bean>
|
||||
|
||||
<!-- Inject the SolrLoggerUsageEventListener into the EventService -->
|
||||
<bean class="org.dspace.statistics.SolrLoggerUsageEventListener">
|
||||
<property name="eventService" ref="org.dspace.services.EventService"/>
|
||||
</bean>
|
||||
|
||||
<!-- Google Analytics recording -->
|
||||
<bean class="org.dspace.google.GoogleRecorderEventListener">
|
||||
<property name="eventService" ref="org.dspace.services.EventService"/>
|
||||
</bean>
|
||||
|
||||
</beans>
|
Reference in New Issue
Block a user