Merge branch 'main' into CST-5306

This commit is contained in:
Giuseppe Digilio
2022-06-08 16:59:33 +02:00
65 changed files with 1983 additions and 209 deletions

View File

@@ -31,6 +31,11 @@ jobs:
# We turn off 'latest' tag by default.
TAGS_FLAVOR: |
latest=false
# Architectures / Platforms for which we will build Docker images
# If this is a PR, we ONLY build for AMD64. For PRs we only do a sanity check test to ensure Docker builds work.
# If this is NOT a PR (e.g. a tag or merge commit), also build for ARM64. NOTE: The ARM64 build takes MUCH
# longer (around 45mins or so) which is why we only run it when pushing a new Docker image.
PLATFORMS: linux/amd64${{ github.event_name != 'pull_request' && ', linux/arm64' || '' }}
steps:
# https://github.com/actions/checkout
@@ -42,7 +47,7 @@ jobs:
uses: docker/setup-buildx-action@v1
# https://github.com/docker/setup-qemu-action
- name: Set up QEMU
- name: Set up QEMU emulation to build for multiple architectures
uses: docker/setup-qemu-action@v2
# https://github.com/docker/login-action
@@ -74,7 +79,7 @@ jobs:
with:
context: .
file: ./Dockerfile.dependencies
platforms: linux/amd64,linux/arm64
platforms: ${{ env.PLATFORMS }}
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
# but we ONLY do an image push to DockerHub if it's NOT a PR
push: ${{ github.event_name != 'pull_request' }}
@@ -100,7 +105,7 @@ jobs:
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
platforms: ${{ env.PLATFORMS }}
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
# but we ONLY do an image push to DockerHub if it's NOT a PR
push: ${{ github.event_name != 'pull_request' }}
@@ -129,7 +134,7 @@ jobs:
with:
context: .
file: ./Dockerfile.test
platforms: linux/amd64,linux/arm64
platforms: ${{ env.PLATFORMS }}
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
# but we ONLY do an image push to DockerHub if it's NOT a PR
push: ${{ github.event_name != 'pull_request' }}
@@ -155,7 +160,7 @@ jobs:
with:
context: .
file: ./Dockerfile.cli
platforms: linux/amd64,linux/arm64
platforms: ${{ env.PLATFORMS }}
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
# but we ONLY do an image push to DockerHub if it's NOT a PR
push: ${{ github.event_name != 'pull_request' }}

View File

@@ -361,6 +361,23 @@
<artifactId>ehcache</artifactId>
<version>${ehcache.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-cache
Caching dependencies for sherpa service. -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>${spring-boot.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
@@ -862,6 +879,13 @@
<artifactId>mockserver-junit-rule</artifactId>
<version>5.11.2</version>
<scope>test</scope>
<exclusions>
<!-- Exclude snakeyaml to avoid conflicts with: spring-boot-starter-cache -->
<exclusion>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

View File

@@ -31,6 +31,7 @@ import org.dspace.app.sherpa.v2.SHERPAResponse;
import org.dspace.app.sherpa.v2.SHERPAUtils;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
/**
* SHERPAService is responsible for making the HTTP call to the SHERPA v2 API
@@ -43,6 +44,7 @@ import org.springframework.beans.factory.annotation.Autowired;
* @author Kim Shepherd
*/
public class SHERPAService {
private CloseableHttpClient client = null;
private int maxNumberOfTries;
@@ -91,6 +93,7 @@ public class SHERPAService {
* @param query ISSN string to pass in an "issn equals" API query
* @return SHERPAResponse containing an error or journal policies
*/
@Cacheable(key = "#query", cacheNames = "sherpa.searchByJournalISSN")
public SHERPAResponse searchByJournalISSN(String query) {
return performRequest("publication", "issn", "equals", query, 0, 1);
}
@@ -413,4 +416,5 @@ public class SHERPAService {
public void setTimeout(int timeout) {
this.timeout = timeout;
}
}
}

View File

@@ -0,0 +1,71 @@
/**
* 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.sherpa.cache;
import java.util.Objects;
import java.util.Set;
import org.dspace.app.sherpa.submit.SHERPASubmitService;
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.springframework.cache.CacheManager;
/**
* This service is responsible to deal with the SherpaService cache.
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com)
*/
public class SherpaCacheEvictService {
// The cache that is managed by this service.
static final String CACHE_NAME = "sherpa.searchByJournalISSN";
private CacheManager cacheManager;
private SHERPASubmitService sherpaSubmitService;
/**
* Remove immediately from the cache all the response that are related to a specific item
* extracting the ISSNs from the item
*
* @param context The DSpace context
* @param item an Item
*/
public void evictCacheValues(Context context, Item item) {
Set<String> ISSNs = sherpaSubmitService.getISSNs(context, item);
for (String issn : ISSNs) {
Objects.requireNonNull(cacheManager.getCache(CACHE_NAME)).evictIfPresent(issn);
}
}
/**
* Invalidate immediately the Sherpa cache
*/
public void evictAllCacheValues() {
Objects.requireNonNull(cacheManager.getCache(CACHE_NAME)).invalidate();
}
/**
* Set the reference to the cacheManager
*
* @param cacheManager
*/
public void setCacheManager(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
/**
* Set the reference to the SherpaSubmitService
*
* @param sherpaSubmitService
*/
public void setSherpaSubmitService(SHERPASubmitService sherpaSubmitService) {
this.sherpaSubmitService = sherpaSubmitService;
}
}

View File

@@ -0,0 +1,34 @@
/**
* 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.sherpa.cache;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ehcache.event.CacheEvent;
import org.ehcache.event.CacheEventListener;
/**
* This is a EHCache listner responsible for logging sherpa cache events. It is
* bound to the sherpa cache via the dspace/config/ehcache.xml file. We need a
* dedicated Logger for each cache as the CacheEvent doesn't include details
* about where the event occur
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com)
*
*/
public class SherpaCacheLogger implements CacheEventListener<Object, Object> {
private static final Logger log = LogManager.getLogger(SherpaCacheLogger.class);
@Override
public void onEvent(CacheEvent<?, ?> cacheEvent) {
log.debug("Sherpa Cache Event Type: {} | Key: {} ",
cacheEvent.getType(), cacheEvent.getKey());
}
}

View File

@@ -9,7 +9,6 @@ package org.dspace.app.sherpa.submit;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -63,19 +62,19 @@ public class SHERPASubmitService {
* issnItemExtractor(s) in the SHERPA spring configuration.
* The ISSNs are not validated with a regular expression or other rules - any values
* extracted will be included in API queries.
* Return the first not empty response from Sherpa
* @see "dspace-dspace-addon-sherpa-configuration-services.xml"
* @param context DSpace context
* @param item DSpace item containing ISSNs to be checked
* @return SHERPA v2 API response (policy data)
*/
public List<SHERPAResponse> searchRelatedJournals(Context context, Item item) {
public SHERPAResponse searchRelatedJournals(Context context, Item item) {
Set<String> issns = getISSNs(context, item);
if (issns == null || issns.size() == 0) {
return null;
} else {
// SHERPA v2 API no longer supports "OR'd" ISSN search, perform individual searches instead
Iterator<String> issnIterator = issns.iterator();
List<SHERPAResponse> responses = new LinkedList<>();
while (issnIterator.hasNext()) {
String issn = issnIterator.next();
SHERPAResponse response = sherpaService.searchByJournalISSN(issn);
@@ -83,14 +82,13 @@ public class SHERPASubmitService {
// Continue with loop
log.warn("Failed to look up SHERPA ROMeO result for ISSN: " + issn
+ ": " + response.getMessage());
return response;
} else if (!response.getJournals().isEmpty()) {
// return this response, if it is not empty
return response;
}
// Store this response, even if it has an error (useful for UI reporting)
responses.add(response);
}
if (responses.isEmpty()) {
responses.add(new SHERPAResponse("SHERPA ROMeO lookup failed"));
}
return responses;
return new SHERPAResponse();
}
}

View File

@@ -0,0 +1,45 @@
/**
* 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.sherpa.v2;
import java.io.Serializable;
/**
* Model class for the Embargo of SHERPAv2 API (JSON)
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com)
*/
public class SHERPAEmbargo implements Serializable {
private static final long serialVersionUID = 6140668058547523656L;
private int amount;
private String units;
public SHERPAEmbargo(int amount, String units) {
this.amount = amount;
this.units = units;
}
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
}
public String getUnits() {
return units;
}
public void setUnits(String units) {
this.units = units;
}
}

View File

@@ -7,6 +7,7 @@
*/
package org.dspace.app.sherpa.v2;
import java.io.Serializable;
import java.util.List;
/**
@@ -21,7 +22,7 @@ import java.util.List;
*
* @author Kim Shepherd
*/
public class SHERPAJournal {
public class SHERPAJournal implements Serializable {
private List<String> titles;
private String url;

View File

@@ -7,6 +7,7 @@
*/
package org.dspace.app.sherpa.v2;
import java.io.Serializable;
import java.util.List;
/**
@@ -28,7 +29,9 @@ import java.util.List;
*
* @see SHERPAPublisherPolicy
*/
public class SHERPAPermittedVersion {
public class SHERPAPermittedVersion implements Serializable {
private static final long serialVersionUID = 4992181606327727442L;
// Version (submitted, accepted, published)
private String articleVersion;
@@ -47,11 +50,6 @@ public class SHERPAPermittedVersion {
// Embargo
private SHERPAEmbargo embargo;
protected static class SHERPAEmbargo {
String units;
int amount;
}
public String getArticleVersion() {
return articleVersion;
}

View File

@@ -7,6 +7,8 @@
*/
package org.dspace.app.sherpa.v2;
import java.io.Serializable;
/**
* Plain java representation of a SHERPA Publisher object, based on SHERPA API v2 responses.
*
@@ -18,7 +20,7 @@ package org.dspace.app.sherpa.v2;
* @see SHERPAJournal
* @see SHERPAPublisherResponse
*/
public class SHERPAPublisher {
public class SHERPAPublisher implements Serializable {
private String name = null;
private String relationshipType;
private String country;

View File

@@ -7,6 +7,7 @@
*/
package org.dspace.app.sherpa.v2;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
@@ -22,7 +23,7 @@ import java.util.Map;
* @see SHERPAJournal
* @see SHERPAPermittedVersion
*/
public class SHERPAPublisherPolicy {
public class SHERPAPublisherPolicy implements Serializable {
private int id;
private boolean openAccessPermitted;

View File

@@ -10,12 +10,15 @@ package org.dspace.app.sherpa.v2;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.JSONArray;
@@ -33,7 +36,10 @@ import org.json.JSONTokener;
* @author Kim Shepherd
*
*/
public class SHERPAResponse {
public class SHERPAResponse implements Serializable {
private static final long serialVersionUID = 2732963970169240597L;
// Is this response to be treated as an error?
private boolean error;
@@ -52,6 +58,9 @@ public class SHERPAResponse {
// SHERPA URI (the human page version of this API response)
private String uri;
@JsonIgnore
private Date retrievalTime = new Date();
// Format enum - currently only JSON is supported
public enum SHERPAFormat {
JSON, XML
@@ -71,6 +80,11 @@ public class SHERPAResponse {
}
}
/**
* Create an empty SHERPAResponse representation
*/
public SHERPAResponse() {}
/**
* Parse the SHERPA v2 API JSON and construct Romeo policy data for display
* This method does not return a value, but rather populates the metadata and journals objects
@@ -479,6 +493,12 @@ public class SHERPAResponse {
}
permittedVersion.setLicenses(sherpaLicenses);
if (permitted.has("embargo")) {
JSONObject embargo = permitted.getJSONObject("embargo");
SHERPAEmbargo SHERPAEmbargo = new SHERPAEmbargo(embargo.getInt("amount"), embargo.getString("units"));
permittedVersion.setEmbargo(SHERPAEmbargo);
}
return permittedVersion;
}
@@ -542,4 +562,8 @@ public class SHERPAResponse {
public SHERPASystemMetadata getMetadata() {
return metadata;
}
public Date getRetrievalTime() {
return retrievalTime;
}
}

View File

@@ -7,6 +7,8 @@
*/
package org.dspace.app.sherpa.v2;
import java.io.Serializable;
/**
* Plain java representation of a SHERPA System Metadata object, based on SHERPA API v2 responses.
*
@@ -18,7 +20,7 @@ package org.dspace.app.sherpa.v2;
*
* @author Kim Shepherd
*/
public class SHERPASystemMetadata {
public class SHERPASystemMetadata implements Serializable {
private int id;
private String uri;

View File

@@ -50,7 +50,7 @@ public class AuthoritySolrServiceImpl implements AuthorityIndexingService, Autho
*/
protected SolrClient solr = null;
protected SolrClient getSolr()
public SolrClient getSolr()
throws MalformedURLException, SolrServerException, IOException {
if (solr == null) {
@@ -67,7 +67,11 @@ public class AuthoritySolrServiceImpl implements AuthorityIndexingService, Autho
SolrQuery solrQuery = new SolrQuery().setQuery("*:*");
solrServer.query(solrQuery);
try {
solrServer.query(solrQuery);
} catch (Exception ex) {
log.error("An error occurs querying authority solr core", ex);
}
solr = solrServer;
}

View File

@@ -657,6 +657,15 @@ public class Context implements AutoCloseable {
return myGroups;
}
/**
* Get a set of all of the special groups uuids that current user is a member of.
*
* @return list of special groups uuids
*/
public Set<UUID> getSpecialGroupUuids() {
return CollectionUtils.isEmpty(specialGroups) ? Set.of() : specialGroups;
}
/**
* Temporary change the user bound to the context, empty the special groups that
* are retained to allow subsequent restore

View File

@@ -113,9 +113,11 @@ public abstract class IndexFactoryImpl<T extends IndexableObject, S> implements
log.info("Full text is larger than the configured limit (discovery.solr.fulltext.charLimit)."
+ " Only the first {} characters were indexed.", charLimit);
} else {
log.error("Tika parsing error. Could not index full text.", saxe);
throw new IOException("Tika parsing error. Could not index full text.", saxe);
}
} catch (TikaException ex) {
log.error("Tika parsing error. Could not index full text.", ex);
throw new IOException("Tika parsing error. Could not index full text.", ex);
}

View File

@@ -569,4 +569,9 @@ public class EPersonServiceImpl extends DSpaceObjectServiceImpl<EPerson> impleme
public int countTotal(Context context) throws SQLException {
return ePersonDAO.countRows(context);
}
@Override
public String getName(EPerson dso) {
return dso.getName();
}
}

View File

@@ -829,4 +829,9 @@ public class GroupServiceImpl extends DSpaceObjectServiceImpl<Group> implements
final MetadataField metadataField) throws SQLException {
return groupDAO.findByMetadataField(context, searchValue, metadataField);
}
@Override
public String getName(Group dso) {
return dso.getName();
}
}

View File

@@ -6,7 +6,7 @@
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.cache;
package org.dspace.iiif.logger;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

View File

@@ -5,7 +5,7 @@
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.cache;
package org.dspace.iiif.logger;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

View File

@@ -0,0 +1,57 @@
/**
* 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.statistics;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import com.maxmind.geoip2.DatabaseReader;
import org.apache.commons.lang3.StringUtils;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Service that handle the GeoIP database file.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class GeoIpService {
@Autowired
private ConfigurationService configurationService;
/**
* Returns an instance of {@link DatabaseReader} based on the configured db
* file, if any.
*
* @return the Database reader
* @throws IllegalStateException if the db file is not configured correctly
*/
public DatabaseReader getDatabaseReader() throws IllegalStateException {
String dbPath = configurationService.getProperty("usage-statistics.dbfile");
if (StringUtils.isBlank(dbPath)) {
throw new IllegalStateException("The required 'dbfile' configuration is missing in solr-statistics.cfg!");
}
try {
File dbFile = new File(dbPath);
return new DatabaseReader.Builder(dbFile).build();
} catch (FileNotFoundException fe) {
throw new IllegalStateException(
"The GeoLite Database file is missing (" + dbPath + ")! Solr Statistics cannot generate location " +
"based reports! Please see the DSpace installation instructions for instructions to install " +
"this file.",fe);
} catch (IOException e) {
throw new IllegalStateException(
"Unable to load GeoLite Database file (" + dbPath + ")! You may need to reinstall it. See the " +
"DSpace installation instructions for more details.", e);
}
}
}

View File

@@ -8,7 +8,6 @@
package org.dspace.statistics;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
@@ -142,6 +141,8 @@ public class SolrLoggerServiceImpl implements SolrLoggerService, InitializingBea
private ClientInfoService clientInfoService;
@Autowired
private SolrStatisticsCore solrStatisticsCore;
@Autowired
private GeoIpService geoIpService;
/** URL to the current-year statistics core. Prior-year shards will have a year suffixed. */
private String statisticsCoreURL;
@@ -179,26 +180,10 @@ public class SolrLoggerServiceImpl implements SolrLoggerService, InitializingBea
//spiderIps = SpiderDetector.getSpiderIpAddresses();
DatabaseReader service = null;
// Get the db file for the location
String dbPath = configurationService.getProperty("usage-statistics.dbfile");
if (dbPath != null) {
try {
File dbFile = new File(dbPath);
service = new DatabaseReader.Builder(dbFile).build();
} catch (FileNotFoundException fe) {
log.error(
"The GeoLite Database file is missing (" + dbPath + ")! Solr Statistics cannot generate location " +
"based reports! Please see the DSpace installation instructions for instructions to install " +
"this file.",
fe);
} catch (IOException e) {
log.error(
"Unable to load GeoLite Database file (" + dbPath + ")! You may need to reinstall it. See the " +
"DSpace installation instructions for more details.",
e);
}
} else {
log.error("The required 'dbfile' configuration is missing in solr-statistics.cfg!");
try {
service = geoIpService.getDatabaseReader();
} catch (IllegalStateException ex) {
log.error(ex);
}
locationService = service;
}

View File

@@ -1,45 +0,0 @@
<?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.app.sherpa.submit.SHERPASubmitConfigurationService"
id="org.dspace.app.sherpa.submit.SHERPASubmitConfigurationService">
<property name="issnItemExtractors">
<list>
<bean class="org.dspace.app.sherpa.submit.MetadataValueISSNExtractor">
<property name="metadataList">
<list>
<value>dc.identifier.issn</value>
</list>
</property>
</bean>
<!-- Use the follow if you have the SHERPARoMEOJournalTitle enabled
<bean class="org.dspace.app.sherpa.submit.MetadataAuthorityISSNExtractor">
<property name="metadataList">
<list>
<value>dc.title.alternative</value>
</list>
</property>
</bean> -->
</list>
</property>
</bean>
</beans>

View File

@@ -25,10 +25,38 @@
<property name="timeout" value="5000"/>
</bean>
<bean class="org.dspace.app.sherpa.submit.SHERPASubmitService"
id="org.dspace.app.sherpa.submit.SHERPASubmitService">
<property name="sherpaService" ref="org.dspace.app.sherpa.SHERPAService"/>
<property name="configuration" ref="org.dspace.app.sherpa.submit.SHERPASubmitConfigurationService"/>
</bean>
<bean class="org.dspace.app.sherpa.submit.SHERPASubmitConfigurationService"
id="org.dspace.app.sherpa.submit.SHERPASubmitConfigurationService">
<property name="issnItemExtractors">
<list>
<bean class="org.dspace.app.sherpa.submit.MetadataValueISSNExtractor">
<property name="metadataList">
<list>
<value>dc.identifier.issn</value>
</list>
</property>
</bean>
<!-- Use the follow if you have the SHERPARoMEOJournalTitle enabled
<bean class="org.dspace.app.sherpa.submit.MetadataAuthorityISSNExtractor">
<property name="metadataList">
<list>
<value>dc.title.alternative</value>
</list>
</property>
</bean> -->
</list>
</property>
</bean>
<bean class="org.dspace.app.sherpa.cache.SherpaCacheEvictService">
<property name="sherpaSubmitService"
ref="org.dspace.app.sherpa.submit.SHERPASubmitService"/>
<property name="cacheManager" ref="cacheManager" />
</bean>
</beans>

View File

@@ -137,6 +137,12 @@
<processing-class>org.dspace.app.rest.submit.step.DescribeStep</processing-class>
<type>submission-form</type>
</step-definition>
<step-definition id="sherpaPolicies" mandatory="true">
<heading>submit.progressbar.sherpapolicy</heading>
<processing-class>org.dspace.app.rest.submit.step.SherpaPolicyStep</processing-class>
<type>sherpaPolicy</type>
</step-definition>
</step-definitions>
<!-- The submission-definitions map lays out the detailed definition of -->
@@ -172,6 +178,7 @@
<!-- <step id="upload-with-embargo"/> -->
<!-- <step id="extractionstep"/> -->
<step id="defaultAC"/>
<step id="sherpaPolicies"/>
<!--Step will be to Sign off on the License -->
<step id="license"/>

View File

@@ -145,6 +145,9 @@ useProxies = true
proxies.trusted.ipranges = 7.7.7.7
proxies.trusted.include_ui_ip = true
# For the tests we have to disable this health indicator because there isn't a mock server and the calculated status was DOWN
management.health.solrOai.enabled = false
researcher-profile.entity-type = Person
# Configuration settings required for Researcher Profiles

View File

@@ -15,13 +15,7 @@
<!-- SHERPA data providers set up to use mock SHERPA service -->
<bean class="org.dspace.external.provider.impl.SHERPAv2JournalISSNDataProvider" init-method="init">
<property name="sourceIdentifier" value="sherpaJournalIssn"/>
<property name="sherpaService">
<bean class="org.dspace.app.sherpa.MockSHERPAService">
<property name="maxNumberOfTries" value="3"/>
<property name="sleepBetweenTimeouts" value="2000"/>
<property name="timeout" value="5000"/>
</bean>
</property>
<property name="sherpaService" ref="org.dspace.app.sherpa.MockSHERPAService" />
<property name="supportedEntityTypes">
<list>
<value>Journal</value>
@@ -30,13 +24,7 @@
</bean>
<bean class="org.dspace.external.provider.impl.SHERPAv2JournalDataProvider" init-method="init">
<property name="sourceIdentifier" value="sherpaJournal"/>
<property name="sherpaService">
<bean class="org.dspace.app.sherpa.MockSHERPAService">
<property name="maxNumberOfTries" value="3"/>
<property name="sleepBetweenTimeouts" value="2000"/>
<property name="timeout" value="5000"/>
</bean>
</property>
<property name="sherpaService" ref="org.dspace.app.sherpa.MockSHERPAService" />
<property name="supportedEntityTypes">
<list>
<value>Journal</value>
@@ -45,13 +33,7 @@
</bean>
<bean class="org.dspace.external.provider.impl.SHERPAv2PublisherDataProvider" init-method="init">
<property name="sourceIdentifier" value="sherpaPublisher"/>
<property name="sherpaService">
<bean class="org.dspace.app.sherpa.MockSHERPAService">
<property name="maxNumberOfTries" value="3"/>
<property name="sleepBetweenTimeouts" value="2000"/>
<property name="timeout" value="5000"/>
</bean>
</property>
<property name="sherpaService" ref="org.dspace.app.sherpa.MockSHERPAService" />
<property name="supportedEntityTypes">
<list>
<value>OrgUnit</value>

View File

@@ -33,4 +33,18 @@
</property>
</bean>
<!-- during test we need the mock sherpa service everywhere -->
<alias name="org.dspace.app.sherpa.SHERPAService" alias="org.dspace.app.sherpa.MockSHERPAService"/>
<bean class="org.dspace.app.sherpa.MockSHERPAService" id="org.dspace.app.sherpa.SHERPAService">
<property name="maxNumberOfTries" value="3"/>
<property name="sleepBetweenTimeouts" value="2000"/>
<property name="timeout" value="5000"/>
</bean>
<bean class="org.dspace.app.sherpa.submit.SHERPASubmitService"
id="org.dspace.app.sherpa.submit.SHERPASubmitService">
<property name="sherpaService" ref="org.dspace.app.sherpa.MockSHERPAService"/>
<property name="configuration" ref="org.dspace.app.sherpa.submit.SHERPASubmitConfigurationService"/>
</bean>
</beans>

View File

@@ -47,5 +47,7 @@
<bean id="org.dspace.statistics.SolrStatisticsCore"
class="org.dspace.statistics.MockSolrStatisticsCore"
autowire-candidate="true"/>
<bean class="org.dspace.statistics.GeoIpService" autowire-candidate="true"/>
</beans>

View File

@@ -1,37 +0,0 @@
<?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.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd"
default-autowire-candidates="*Service,*DAO,javax.sql.DataSource">
<context:annotation-config/> <!-- allows us to use spring annotations in beans -->
<!-- during test we need the mock sherpa service everywhere -->
<alias name="org.dspace.app.sherpa.SHERPAService" alias="org.dspace.app.sherpa.MockSHERPAService"/>
<bean class="org.dspace.app.sherpa.MockSHERPAService" id="org.dspace.app.sherpa.SHERPAService">
<property name="maxNumberOfTries" value="3"/>
<property name="sleepBetweenTimeouts" value="2000"/>
<property name="timeout" value="5000"/>
</bean>
<bean class="org.dspace.app.sherpa.submit.SHERPASubmitService"
id="org.dspace.app.sherpa.submit.SHERPASubmitService">
<property name="sherpaService" ref="org.dspace.app.sherpa.MockSHERPAService"/>
<property name="configuration" ref="org.dspace.app.sherpa.submit.SHERPASubmitConfigurationService"/>
</bean>
</beans>

View File

@@ -11,6 +11,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Objects;
import org.dspace.app.sherpa.v2.SHERPAPublisherResponse;
import org.dspace.app.sherpa.v2.SHERPAResponse;
@@ -25,20 +26,6 @@ import org.dspace.app.sherpa.v2.SHERPAResponse;
*/
public class MockSHERPAService extends SHERPAService {
/**
* Simple overridden 'searchByJournalISSN' so that we do attempt to build the URI but rather than make
* an actual HTTP call, return parsed SHERPAResponse for The Lancet based on known-good JSON stored with our
* test resources.
* If URI creation, parsing, or IO fails along the way, a SHERPAResponse with an error message set will be
* returned.
* @param query ISSN string to pass in an "issn equals" API query
* @return SHERPAResponse
*/
@Override
public SHERPAResponse searchByJournalISSN(String query) {
return performRequest("publication", "issn", "equals", query, 0, 1);
}
/**
* Simple overridden performRequest so that we do attempt to build the URI but rather than make
* an actual HTTP call, return parsed SHERPAResponse for The Lancet based on known-good JSON stored with our
@@ -67,8 +54,12 @@ public class MockSHERPAService extends SHERPAService {
return new SHERPAResponse("Error building URI");
}
// Get mock JSON - in this case, a known good result for The Lancet
content = getClass().getResourceAsStream("thelancet.json");
// Get mock JSON
// if a file with the name contained in the value does not exist, returns thelancet.json
content = getContent(value.concat(".json"));
if (Objects.isNull(content)) {
content = getContent("thelancet.json");
}
// Parse JSON input stream and return response for later evaluation
return new SHERPAResponse(content, SHERPAResponse.SHERPAFormat.JSON);
@@ -88,6 +79,10 @@ public class MockSHERPAService extends SHERPAService {
}
}
private InputStream getContent(String fileName) {
return getClass().getResourceAsStream(fileName);
}
/**
* Simple overridden performPublisherRequest so that we do attempt to build the URI but rather than make
* an actual HTTP call, return parsed SHERPAPublisherResponse for PLOS based on known-good JSON stored with our
@@ -133,4 +128,5 @@ public class MockSHERPAService extends SHERPAService {
return new SHERPAPublisherResponse(e.getMessage());
}
}
}

View File

@@ -11,7 +11,6 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.sql.SQLException;
import java.util.List;
import org.dspace.AbstractUnitTest;
import org.dspace.app.sherpa.v2.SHERPAResponse;
@@ -109,20 +108,18 @@ public class SHERPASubmitServiceTest extends AbstractUnitTest {
// Get responses from SHERPA submit service, which should inspect item ISSNs and perform search
// on the mock SHERPA service
List<SHERPAResponse> responses = sherpaSubmitService.searchRelatedJournals(context, testItem);
SHERPAResponse response = sherpaSubmitService.searchRelatedJournals(context, testItem);
// Make sure response is not null or empty
assertTrue("Response list should not be null or empty",
responses != null && !responses.isEmpty());
assertTrue("Response should not be null", response != null);
// For each response (there should be only one based on test data) perform the standard set
// of thorough parsing tests
for (SHERPAResponse response : responses) {
// Assert response is not error, or fail with message
assertFalse("Response was flagged as 'isError'", response.isError());
// Skip remainder of parsing tests - these are already done in SHERPAServiceTEst
}
// Assert response is not error, or fail with message
assertFalse("Response was flagged as 'isError'", response.isError());
// Skip remainder of parsing tests - these are already done in SHERPAServiceTEst
}
}

View File

@@ -177,14 +177,18 @@ public class WorkspaceItemBuilder extends AbstractBuilder<WorkspaceItem, Workspa
return addMetadataValue(MetadataSchemaEnum.DC.getName(), "subject", null, subject);
}
public WorkspaceItemBuilder withAbstract(final String subject) {
return addMetadataValue(MetadataSchemaEnum.DC.getName(),"description", "abstract", subject);
public WorkspaceItemBuilder withIssn(String issn) {
return addMetadataValue(MetadataSchemaEnum.DC.getName(), "identifier", "issn", issn);
}
public WorkspaceItemBuilder withEntityType(final String entityType) {
return addMetadataValue("dspace", "entity", "type", entityType);
}
public WorkspaceItemBuilder withAbstract(final String subject) {
return addMetadataValue(MetadataSchemaEnum.DC.getName(),"description", "abstract", subject);
}
public WorkspaceItemBuilder grantLicense() {
Item item = workspaceItem.getItem();
String license;

View File

@@ -0,0 +1,3 @@
{
"items": []
}

View File

@@ -0,0 +1,504 @@
{
"items": [
{
"system_metadata": {
"id": 40863,
"uri": "https://v2.sherpa.ac.uk/id/publication/40863",
"date_modified": "2022-03-25 14:08:29",
"publicly_visible": "yes",
"publicly_visible_phrases": [
{
"language": "en",
"phrase": "Yes",
"value": "yes"
}
],
"date_created": "2022-01-11 09:43:53"
},
"tj_status_phrases": [
{
"phrase": "Plan S Approved",
"value": "plan_s_approved",
"language": "en"
}
],
"type_phrases": [
{
"value": "journal",
"phrase": "Journal",
"language": "en"
}
],
"id": 40863,
"issns": [
{
"issn": "2731-0582"
}
],
"publishers": [
{
"relationship_type": "commercial_publisher",
"relationship_type_phrases": [
{
"value": "commercial_publisher",
"phrase": "Commercial Publisher",
"language": "en"
}
],
"publisher": {
"id": 3286,
"name": [
{
"name": "Nature Research",
"language": "en",
"preferred_phrases": [
{
"language": "en",
"phrase": "Name",
"value": "name"
}
],
"preferred": "name",
"language_phrases": [
{
"phrase": "English",
"value": "en",
"language": "en"
}
]
}
],
"imprint_of_id": 62037,
"country": "gb",
"country_phrases": [
{
"value": "gb",
"phrase": "United Kingdom",
"language": "en"
}
],
"publication_count": 87,
"uri": "https://v2.sherpa.ac.uk/id/publisher/3286",
"url": "https://www.nature.com/"
}
}
],
"listed_in_doaj_phrases": [
{
"language": "en",
"phrase": "No",
"value": "no"
}
],
"listed_in_doaj": "no",
"tj_status": [
"plan_s_approved"
],
"publisher_policy": [
{
"open_access_prohibited": "no",
"id": 3286,
"publication_count": 36,
"internal_moniker": "Default Policy",
"urls": [
{
"description": "Self archiving and license to publish",
"url": "https://www.nature.com/neuro/editorial-policies/self-archiving-and-license-to-publish"
},
{
"description": "Preprints and Conference Proceedings",
"url": "https://www.nature.com/nature-portfolio/editorial-policies/preprints-and-conference-proceedings"
},
{
"url": "https://www.springernature.com/gp/open-research/policies/accepted-manuscript-terms",
"description": "Accepted manuscript terms of use"
}
],
"open_access_prohibited_phrases": [
{
"value": "no",
"phrase": "No",
"language": "en"
}
],
"uri": "https://v2.sherpa.ac.uk/id/publisher_policy/3286",
"permitted_oa": [
{
"prerequisites": {
"prerequisites_phrases": [
{
"language": "en",
"value": "when_research_article",
"phrase": "If a Research Article"
}
],
"prerequisites": [
"when_research_article"
]
},
"copyright_owner": "authors",
"additional_oa_fee_phrases": [
{
"language": "en",
"value": "no",
"phrase": "No"
}
],
"article_version_phrases": [
{
"language": "en",
"value": "submitted",
"phrase": "Submitted"
}
],
"additional_oa_fee": "no",
"copyright_owner_phrases": [
{
"language": "en",
"value": "authors",
"phrase": "Authors"
}
],
"article_version": [
"submitted"
],
"location": {
"location_phrases": [
{
"value": "authors_homepage",
"phrase": "Author's Homepage",
"language": "en"
},
{
"language": "en",
"phrase": "Funder Designated Location",
"value": "funder_designated_location"
},
{
"language": "en",
"value": "institutional_repository",
"phrase": "Institutional Repository"
},
{
"phrase": "Preprint Repository",
"value": "preprint_repository",
"language": "en"
}
],
"location": [
"authors_homepage",
"funder_designated_location",
"institutional_repository",
"preprint_repository"
]
},
"conditions": [
"Must link to publisher version",
"Upon publication, source must be acknowledged and DOI cited",
"Post-prints are subject to Springer Nature re-use terms",
"Non-commercial use only"
]
},
{
"embargo": {
"units": "months",
"amount": 6,
"units_phrases": [
{
"phrase": "Months",
"value": "months",
"language": "en"
}
]
},
"license": [
{
"license_phrases": [
{
"phrase": "Publisher's Bespoke License",
"value": "bespoke_license",
"language": "en"
}
],
"license": "bespoke_license"
}
],
"article_version_phrases": [
{
"value": "accepted",
"phrase": "Accepted",
"language": "en"
}
],
"additional_oa_fee": "no",
"conditions": [
"Must link to publisher version",
"Published source must be acknowledged and DOI cited",
"Post-prints are subject to Springer Nature re-use terms",
"Non-commercial use only"
],
"copyright_owner_phrases": [
{
"phrase": "Authors",
"value": "authors",
"language": "en"
}
],
"location": {
"location": [
"authors_homepage",
"funder_designated_location",
"institutional_repository",
"named_repository"
],
"location_phrases": [
{
"phrase": "Author's Homepage",
"value": "authors_homepage",
"language": "en"
},
{
"phrase": "Funder Designated Location",
"value": "funder_designated_location",
"language": "en"
},
{
"language": "en",
"value": "institutional_repository",
"phrase": "Institutional Repository"
},
{
"language": "en",
"value": "named_repository",
"phrase": "Named Repository"
}
],
"named_repository": [
"PubMed Central",
"Europe PMC"
]
},
"article_version": [
"accepted"
],
"prerequisites": {
"prerequisites": [
"when_research_article"
],
"prerequisites_phrases": [
{
"value": "when_research_article",
"phrase": "If a Research Article",
"language": "en"
}
]
},
"copyright_owner": "authors",
"additional_oa_fee_phrases": [
{
"language": "en",
"value": "no",
"phrase": "No"
}
]
}
]
},
{
"id": 4410,
"open_access_prohibited": "no",
"urls": [
{
"url": "https://www.springernature.com/gp/open-research/about/the-fundamentals-of-open-access-and-open-research",
"description": "The fundamentals of open access and open research"
},
{
"url": "https://www.nature.com/neuro/editorial-policies/self-archiving-and-license-to-publish",
"description": "Self archiving and license to publish"
},
{
"url": "https://www.springernature.com/gp/open-research/policies/journal-policies",
"description": "Open access policies for journals"
}
],
"open_access_prohibited_phrases": [
{
"language": "en",
"phrase": "No",
"value": "no"
}
],
"internal_moniker": "Open Access",
"publication_count": 34,
"permitted_oa": [
{
"additional_oa_fee_phrases": [
{
"language": "en",
"phrase": "Yes",
"value": "yes"
}
],
"copyright_owner": "authors",
"conditions": [
"Published source must be acknowledged with citation"
],
"article_version": [
"published"
],
"copyright_owner_phrases": [
{
"language": "en",
"value": "authors",
"phrase": "Authors"
}
],
"location": {
"location_phrases": [
{
"phrase": "Any Website",
"value": "any_website",
"language": "en"
},
{
"language": "en",
"phrase": "Journal Website",
"value": "this_journal"
}
],
"location": [
"any_website",
"this_journal"
]
},
"additional_oa_fee": "yes",
"article_version_phrases": [
{
"phrase": "Published",
"value": "published",
"language": "en"
}
],
"license": [
{
"license_phrases": [
{
"phrase": "CC BY",
"value": "cc_by",
"language": "en"
}
],
"license": "cc_by",
"version": "4.0"
}
],
"publisher_deposit": [
{
"repository_metadata": {
"type_phrases": [
{
"language": "en",
"value": "disciplinary",
"phrase": "Disciplinary"
}
],
"notes": "Launched as UK PubMed Central (UKPMC) in January 2007, changed to Europe PubMed Central in November 2012.\r\nSpecial item types include: Links",
"url": "http://europepmc.org/",
"type": "disciplinary",
"name": [
{
"name": "Europe PMC",
"language": "en",
"preferred": "name",
"language_phrases": [
{
"value": "en",
"phrase": "English",
"language": "en"
}
],
"preferred_phrases": [
{
"language": "en",
"phrase": "Name",
"value": "name"
}
]
}
]
},
"system_metadata": {
"id": 908,
"uri": "https://v2.sherpa.ac.uk/id/repository/908"
}
},
{
"system_metadata": {
"id": 267,
"uri": "https://v2.sherpa.ac.uk/id/repository/267"
},
"repository_metadata": {
"type_phrases": [
{
"language": "en",
"phrase": "Disciplinary",
"value": "disciplinary"
}
],
"type": "disciplinary",
"url": "http://www.ncbi.nlm.nih.gov/pmc/",
"name": [
{
"language": "en",
"name": "PubMed Central",
"preferred": "name",
"language_phrases": [
{
"language": "en",
"value": "en",
"phrase": "English"
}
],
"preferred_phrases": [
{
"language": "en",
"value": "name",
"phrase": "Name"
}
]
}
]
}
}
]
}
],
"uri": "https://v2.sherpa.ac.uk/id/publisher_policy/4410"
}
],
"title": [
{
"preferred_phrases": [
{
"language": "en",
"phrase": "Title",
"value": "name"
}
],
"language_phrases": [
{
"language": "en",
"value": "en",
"phrase": "English"
}
],
"preferred": "name",
"title": "Nature Synthesis",
"language": "en"
}
],
"type": "journal",
"url": "https://www.nature.com/natsynth/"
}
]
}

View File

@@ -72,7 +72,12 @@ public class DSpaceOAIDataProvider {
private DSpaceResumptionTokenFormatter resumptionTokenFormat = new DSpaceResumptionTokenFormatter();
@RequestMapping({"", "/"})
@RequestMapping("")
public void index(HttpServletResponse response, HttpServletRequest request) throws IOException {
response.sendRedirect(request.getRequestURI() + "/");
}
@RequestMapping({"/"})
public String indexAction(HttpServletResponse response, Model model) throws ServletException {
try {
XOAIManager manager = xoaiManagerResolver.getManager();

View File

@@ -271,6 +271,12 @@
<artifactId>spring-boot-starter-aop</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>com.flipkart.zjsonpatch</groupId>
@@ -457,7 +463,6 @@
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
<version>1.1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.ehcache/ehcache -->
<dependency>

View File

@@ -161,6 +161,7 @@ public class Application extends SpringBootServletInitializer {
@Override
public void addCorsMappings(@NonNull CorsRegistry registry) {
// Get allowed origins for api and iiif endpoints.
// The actuator endpoints are configured using management.endpoints.web.cors.* properties
String[] corsAllowedOrigins = configuration
.getCorsAllowedOrigins(configuration.getCorsAllowedOriginsConfig());
String[] iiifAllowedOrigins = configuration

View File

@@ -153,8 +153,9 @@ public class BitstreamRestController {
}
org.dspace.app.rest.utils.BitstreamResource bitstreamResource =
new org.dspace.app.rest.utils.BitstreamResource(
name, uuid, currentUser != null ? currentUser.getID() : null, citationEnabledForBitstream);
new org.dspace.app.rest.utils.BitstreamResource(name, uuid,
currentUser != null ? currentUser.getID() : null,
context.getSpecialGroupUuids(), citationEnabledForBitstream);
//We have all the data we need, close the connection to the database so that it doesn't stay open during
//download/streaming

View File

@@ -0,0 +1,95 @@
/**
* 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.IOException;
import java.net.MalformedURLException;
import java.util.Arrays;
import org.apache.solr.client.solrj.SolrServerException;
import org.dspace.app.rest.DiscoverableEndpointsService;
import org.dspace.app.rest.health.GeoIpHealthIndicator;
import org.dspace.authority.AuthoritySolrServiceImpl;
import org.dspace.discovery.SolrSearchCore;
import org.dspace.statistics.SolrStatisticsCore;
import org.dspace.xoai.services.api.solr.SolrServerResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator;
import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.actuate.solr.SolrHealthIndicator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.hateoas.Link;
/**
* Configuration class related to the actuator endpoints.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
@Configuration
public class ActuatorConfiguration {
public static final Status UP_WITH_ISSUES_STATUS = new Status("UP_WITH_ISSUES");
@Autowired
private DiscoverableEndpointsService discoverableEndpointsService;
@Value("${management.endpoints.web.base-path:/actuator}")
private String actuatorBasePath;
@EventListener(ApplicationReadyEvent.class)
public void registerActuatorEndpoints() {
discoverableEndpointsService.register(this, Arrays.asList(Link.of(actuatorBasePath, "actuator")));
}
@Bean
@ConditionalOnEnabledHealthIndicator("solrSearch")
@ConditionalOnProperty("discovery.search.server")
public SolrHealthIndicator solrSearchCoreHealthIndicator(SolrSearchCore solrSearchCore) {
return new SolrHealthIndicator(solrSearchCore.getSolr());
}
@Bean
@ConditionalOnEnabledHealthIndicator("solrStatistics")
@ConditionalOnProperty("solr-statistics.server")
public SolrHealthIndicator solrStatisticsCoreHealthIndicator(SolrStatisticsCore solrStatisticsCore) {
return new SolrHealthIndicator(solrStatisticsCore.getSolr());
}
@Bean
@ConditionalOnEnabledHealthIndicator("solrAuthority")
@ConditionalOnProperty("solr.authority.server")
public SolrHealthIndicator solrAuthorityCoreHealthIndicator(AuthoritySolrServiceImpl authoritySolrService)
throws MalformedURLException, SolrServerException, IOException {
return new SolrHealthIndicator(authoritySolrService.getSolr());
}
@Bean
@ConditionalOnEnabledHealthIndicator("solrOai")
@ConditionalOnProperty("oai.solr.url")
public SolrHealthIndicator solrOaiCoreHealthIndicator(SolrServerResolver solrServerResolver)
throws SolrServerException {
return new SolrHealthIndicator(solrServerResolver.getServer());
}
@Bean
@ConditionalOnEnabledHealthIndicator("geoIp")
public GeoIpHealthIndicator geoIpHealthIndicator() {
return new GeoIpHealthIndicator();
}
public String getActuatorBasePath() {
return actuatorBasePath;
}
}

View File

@@ -0,0 +1,42 @@
/**
* 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.health;
import static org.dspace.app.rest.configuration.ActuatorConfiguration.UP_WITH_ISSUES_STATUS;
import org.dspace.statistics.GeoIpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health.Builder;
import org.springframework.boot.actuate.health.HealthIndicator;
/**
* Implementation of {@link HealthIndicator} that verifies if the GeoIP database
* is configured correctly.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class GeoIpHealthIndicator extends AbstractHealthIndicator {
@Autowired
private GeoIpService geoIpService;
@Override
protected void doHealthCheck(Builder builder) throws Exception {
try {
geoIpService.getDatabaseReader();
builder.up();
} catch (IllegalStateException ex) {
builder.status(UP_WITH_ISSUES_STATUS).withDetail("reason", ex.getMessage());
}
}
}

View File

@@ -18,10 +18,18 @@ import org.dspace.app.rest.RestResourceController;
* This class acts as a data holder for the RelationshipResource
* Refer to {@link org.dspace.content.Relationship} for explanation about the properties
*/
@LinksRest(links = {
@LinkRest(
name = RelationshipRest.RELATIONSHIP_TYPE,
method = "getRelationshipType"
)
})
public class RelationshipRest extends BaseObjectRest<Integer> {
public static final String NAME = "relationship";
public static final String CATEGORY = "core";
public static final String RELATIONSHIP_TYPE = "relationshipType";
@JsonIgnore
private UUID leftId;
@JsonIgnore

View File

@@ -0,0 +1,48 @@
/**
* 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.model.step;
import java.util.Date;
import org.dspace.app.sherpa.v2.SHERPAResponse;
/**
* Java Bean to expose Sherpa policies during in progress submission.
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com)
*/
public class SherpaPolicy implements SectionData {
private static final long serialVersionUID = 2440249335255683173L;
private Date retrievalTime;
private SHERPAResponse sherpaResponse;
public Date getRetrievalTime() {
return retrievalTime;
}
public void setRetrievalTime(Date retrievalTime) {
this.retrievalTime = retrievalTime;
}
public SHERPAResponse getSherpaResponse() {
return sherpaResponse;
}
/**
* Setting a sherpaResponse will automatically set the retrievealTime
* of the section copying the value from the response if not null
*/
public void setSherpaResponse(SHERPAResponse sherpaResponse) {
this.sherpaResponse = sherpaResponse;
this.retrievalTime = sherpaResponse.getRetrievalTime();
}
}

View File

@@ -0,0 +1,56 @@
/**
* 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.repository;
import java.sql.SQLException;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import org.dspace.app.rest.model.RelationshipRest;
import org.dspace.app.rest.model.RelationshipTypeRest;
import org.dspace.app.rest.projection.Projection;
import org.dspace.content.Relationship;
import org.dspace.content.RelationshipType;
import org.dspace.content.service.RelationshipService;
import org.dspace.core.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
/**
* Link repository for "relationshipType" subresource of an individual Relationship.
*/
@Component(RelationshipRest.CATEGORY + "." + RelationshipRest.NAME + "." + RelationshipRest.RELATIONSHIP_TYPE)
public class RelationshipTypeRelationshipLinkRepository extends AbstractDSpaceRestRepository
implements LinkRestRepository {
@Autowired
RelationshipService relationshipService;
@PreAuthorize("permitAll()")
public RelationshipTypeRest getRelationshipType(@Nullable HttpServletRequest request,
Integer relationshipId,
@Nullable Pageable optionalPageable,
Projection projection) {
try {
Context context = obtainContext();
Relationship relationship = relationshipService.find(context, relationshipId);
if (relationship == null) {
throw new ResourceNotFoundException("No such relationship: " + relationshipId);
}
int total = relationshipService.countByRelationshipType(context, relationship.getRelationshipType());
Pageable pageable = utils.getPageable(optionalPageable);
RelationshipType relationshipType = relationship.getRelationshipType();
return converter.toRest(relationshipType, projection);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -11,6 +11,7 @@ import org.dspace.app.rest.exception.DSpaceAccessDeniedHandler;
import org.dspace.authenticate.service.AuthenticationService;
import org.dspace.services.RequestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
@@ -65,6 +66,9 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private DSpaceAccessDeniedHandler accessDeniedHandler;
@Value("${management.endpoints.web.base-path:/actuator}")
private String actuatorBasePath;
@Override
public void configure(WebSecurity webSecurity) throws Exception {
// Define URL patterns which Spring Security will ignore entirely.
@@ -83,7 +87,7 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
// Configure authentication requirements for ${dspace.server.url}/api/ URL only
// NOTE: REST API is hardcoded to respond on /api/. Other modules (OAI, SWORD, IIIF, etc) use other root paths.
http.requestMatchers()
.antMatchers("/api/**", "/iiif/**")
.antMatchers("/api/**", "/iiif/**", actuatorBasePath + "/**")
.and()
// Enable Spring Security authorization on these paths
.authorizeRequests()
@@ -91,6 +95,7 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
.antMatchers(HttpMethod.POST,"/api/authn/login").permitAll()
// Everyone can call GET on the status endpoint (used to check your authentication status)
.antMatchers(HttpMethod.GET, "/api/authn/status").permitAll()
.antMatchers(HttpMethod.GET, actuatorBasePath + "/info").hasAnyAuthority(ADMIN_GRANT)
.and()
// Tell Spring to not create Sessions
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

View File

@@ -35,5 +35,4 @@ public abstract class AbstractProcessingStep implements DataProcessingStep {
protected MetadataFieldService metadataFieldService = ContentServiceFactory.getInstance().getMetadataFieldService();
protected ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
protected WorkspaceItemService workspaceItemService = ContentServiceFactory.getInstance().getWorkspaceItemService();
}

View File

@@ -0,0 +1,63 @@
/**
* 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.submit.step;
import java.util.Objects;
import javax.servlet.http.HttpServletRequest;
import org.dspace.app.rest.model.patch.Operation;
import org.dspace.app.rest.model.step.SherpaPolicy;
import org.dspace.app.rest.submit.AbstractProcessingStep;
import org.dspace.app.rest.submit.SubmissionService;
import org.dspace.app.sherpa.cache.SherpaCacheEvictService;
import org.dspace.app.sherpa.submit.SHERPASubmitService;
import org.dspace.app.sherpa.v2.SHERPAResponse;
import org.dspace.app.util.SubmissionStepConfig;
import org.dspace.content.InProgressSubmission;
import org.dspace.core.Context;
import org.dspace.utils.DSpace;
import org.dspace.web.ContextUtil;
/**
* SherpaPolicy step for DSpace Spring Rest. Expose information about
* the Sherpa policies for the in progress submission.
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com)
*/
public class SherpaPolicyStep extends AbstractProcessingStep {
public static final String SHERPA_RETRIEVAL_TIME = "retrievalTime";
private SherpaCacheEvictService sherpaCacheEvictService = new DSpace().getSingletonService(
SherpaCacheEvictService.class);
private SHERPASubmitService sherpaSubmitService = new DSpace().getSingletonService(SHERPASubmitService.class);
@Override
@SuppressWarnings("unchecked")
public SherpaPolicy getData(SubmissionService submissionService, InProgressSubmission obj,
SubmissionStepConfig config) throws Exception {
Context context = ContextUtil.obtainCurrentRequestContext();
SHERPAResponse response = sherpaSubmitService.searchRelatedJournals(context, obj.getItem());
if (Objects.nonNull(response)) {
SherpaPolicy result = new SherpaPolicy();
result.setSherpaResponse(response);
return result;
}
return null;
}
@Override
public void doPatchProcessing(Context context, HttpServletRequest currentRequest, InProgressSubmission source,
Operation op, SubmissionStepConfig stepConf) throws Exception {
String path = op.getPath();
if (path.contains(SHERPA_RETRIEVAL_TIME)) {
sherpaCacheEvictService.evictCacheValues(context, source.getItem());
}
}
}

View File

@@ -11,6 +11,7 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.SQLException;
import java.util.Set;
import java.util.UUID;
import org.apache.commons.io.IOUtils;
@@ -40,6 +41,7 @@ public class BitstreamResource extends AbstractResource {
private UUID currentUserUUID;
private boolean shouldGenerateCoverPage;
private byte[] file;
private Set<UUID> currentSpecialGroups;
private BitstreamService bitstreamService = ContentServiceFactory.getInstance().getBitstreamService();
private EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
@@ -47,11 +49,12 @@ public class BitstreamResource extends AbstractResource {
new DSpace().getServiceManager()
.getServicesByType(CitationDocumentService.class).get(0);
public BitstreamResource(String name, UUID uuid, UUID currentUserUUID,
public BitstreamResource(String name, UUID uuid, UUID currentUserUUID, Set<UUID> currentSpecialGroups,
boolean shouldGenerateCoverPage) {
this.name = name;
this.uuid = uuid;
this.currentUserUUID = currentUserUUID;
this.currentSpecialGroups = currentSpecialGroups;
this.shouldGenerateCoverPage = shouldGenerateCoverPage;
}
@@ -84,9 +87,8 @@ public class BitstreamResource extends AbstractResource {
@Override
public InputStream getInputStream() throws IOException {
try (Context context = new Context()) {
EPerson currentUser = ePersonService.find(context, currentUserUUID);
context.setCurrentUser(currentUser);
try (Context context = initializeContext()) {
Bitstream bitstream = bitstreamService.find(context, uuid);
InputStream out;
@@ -110,9 +112,7 @@ public class BitstreamResource extends AbstractResource {
@Override
public long contentLength() throws IOException {
try (Context context = new Context()) {
EPerson currentUser = ePersonService.find(context, currentUserUUID);
context.setCurrentUser(currentUser);
try (Context context = initializeContext()) {
Bitstream bitstream = bitstreamService.find(context, uuid);
if (shouldGenerateCoverPage) {
return getCoverpageByteArray(context, bitstream).length;
@@ -123,4 +123,12 @@ public class BitstreamResource extends AbstractResource {
throw new IOException(e);
}
}
private Context initializeContext() throws SQLException {
Context context = new Context();
EPerson currentUser = ePersonService.find(context, currentUserUUID);
context.setCurrentUser(currentUser);
currentSpecialGroups.forEach(context::setSpecialGroup);
return context;
}
}

View File

@@ -0,0 +1,35 @@
/**
* 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 org.dspace.app.util.Util;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
/**
* Class that use the configuration service to add a property named
* 'dspace.version' with the current DSpace application version.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
@Component
public class DSpaceVersionConfigurationEnricher implements ApplicationRunner {
@Autowired
private ConfigurationService configurationService;
@Override
public void run(ApplicationArguments args) throws Exception {
configurationService.addPropertyValue("dspace.version", Util.getSourceVersion());
}
}

View File

@@ -63,9 +63,9 @@ public class OAIpmhIT extends AbstractControllerIntegrationTest {
private ConfigurationService configurationService;
// All OAI-PMH paths that we test against
private final String ROOT_PATH = "/oai";
private final String ROOT_PATH = "/oai/";
private final String DEFAULT_CONTEXT_PATH = "request";
private final String DEFAULT_CONTEXT = ROOT_PATH + "/" + DEFAULT_CONTEXT_PATH;
private final String DEFAULT_CONTEXT = ROOT_PATH + DEFAULT_CONTEXT_PATH;
// Mock to ensure XOAI caching is disabled for all tests (see @Before method)
@MockBean

View File

@@ -684,6 +684,56 @@ public class BitstreamRestControllerIT extends AbstractControllerIntegrationTest
checkNumberOfStatsRecords(bitstream, 1);
}
@Test
public void restrictedSpecialGroupBitstreamTest() throws Exception {
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.build();
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Collection 1")
.build();
Group restrictedGroup = GroupBuilder.createGroup(context)
.withName("Restricted Group")
.build();
String bitstreamContent = "Private!";
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
Item item = ItemBuilder.createItem(context, col1)
.withTitle("item 1")
.withIssueDate("2013-01-17")
.withAuthor("Doe, John")
.build();
bitstream = BitstreamBuilder
.createBitstream(context, item, is)
.withName("Test Embargoed Bitstream")
.withDescription("This bitstream is embargoed")
.withMimeType("text/plain")
.withReaderGroup(restrictedGroup)
.build();
}
context.restoreAuthSystemState();
String authToken = getAuthToken(eperson.getEmail(), password);
getClient(authToken).perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content"))
.andExpect(status().isForbidden());
configurationService.setProperty("authentication-password.login.specialgroup", "Restricted Group");
authToken = getAuthToken(eperson.getEmail(), password);
getClient(authToken).perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content"))
.andExpect(status().isOk());
checkNumberOfStatsRecords(bitstream, 1);
}
@Test
public void restrictedGroupBitstreamAccessGrantByAdminsTest() throws Exception {
context.turnOffAuthorisationSystem();

View File

@@ -0,0 +1,74 @@
/**
* 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.dspace.app.rest.configuration.ActuatorConfiguration.UP_WITH_ISSUES_STATUS;
import static org.dspace.app.rest.link.search.HealthIndicatorMatcher.match;
import static org.dspace.app.rest.link.search.HealthIndicatorMatcher.matchDatabase;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.Map;
import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
import org.junit.Test;
import org.springframework.boot.actuate.health.Status;
/**
* Integration tests to verify the health indicators configuration.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class HealthIndicatorsIT extends AbstractControllerIntegrationTest {
private static final String HEALTH_PATH = "/actuator/health";
@Test
public void testWithAnonymousUser() throws Exception {
getClient().perform(get(HEALTH_PATH))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status", is(UP_WITH_ISSUES_STATUS.getCode())))
.andExpect(jsonPath("$.components").doesNotExist());
}
@Test
public void testWithNotAdminUser() throws Exception {
String token = getAuthToken(eperson.getEmail(), password);
getClient(token).perform(get(HEALTH_PATH))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status", is(UP_WITH_ISSUES_STATUS.getCode())))
.andExpect(jsonPath("$.components").doesNotExist());
}
@Test
public void testWithAdminUser() throws Exception {
String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(get(HEALTH_PATH))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status", is(UP_WITH_ISSUES_STATUS.getCode())))
.andExpect(jsonPath("$.components", allOf(
matchDatabase(Status.UP),
match("solrSearchCore", Status.UP, Map.of("status", 0, "detectedPathType", "root")),
match("solrStatisticsCore", Status.UP, Map.of("status", 0, "detectedPathType", "root")),
match("geoIp", UP_WITH_ISSUES_STATUS,
Map.of("reason", "The required 'dbfile' configuration is missing in solr-statistics.cfg!"))
)));
}
}

View File

@@ -0,0 +1,81 @@
/**
* 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.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
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.test.AbstractControllerIntegrationTest;
import org.dspace.services.ConfigurationService;
import org.hamcrest.Matcher;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Integration tests for info actuator.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class InfoEndpointIT extends AbstractControllerIntegrationTest {
private static final String INFO_PATH = "/actuator/info";
@Autowired
private ConfigurationService configurationService;
@Test
public void testWithAnonymousUser() throws Exception {
getClient().perform(get(INFO_PATH))
.andExpect(status().isUnauthorized());
}
@Test
public void testWithNotAdminUser() throws Exception {
String token = getAuthToken(eperson.getEmail(), password);
getClient(token).perform(get(INFO_PATH))
.andExpect(status().isForbidden());
}
@Test
public void testWithAdminUser() throws Exception {
String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(get(INFO_PATH))
.andExpect(status().isOk())
.andExpect(jsonPath("$.app.name", matchProperty("dspace.name")))
.andExpect(jsonPath("$.app.dir", matchProperty("dspace.dir")))
.andExpect(jsonPath("$.app.url", matchProperty("dspace.server.url")))
.andExpect(jsonPath("$.app.db", matchProperty("db.url")))
.andExpect(jsonPath("$.app.solr.server", matchProperty("solr.server")))
.andExpect(jsonPath("$.app.solr.prefix", matchProperty("solr.multicorePrefix")))
.andExpect(jsonPath("$.app.mail.server", matchProperty("mail.server")))
.andExpect(jsonPath("$.app.mail.from-address", matchProperty("mail.from.address")))
.andExpect(jsonPath("$.app.mail.feedback-recipient", matchProperty("feedback.recipient")))
.andExpect(jsonPath("$.app.mail.mail-admin", matchProperty("mail.admin")))
.andExpect(jsonPath("$.app.mail.mail-helpdesk", matchProperty("mail.helpdesk")))
.andExpect(jsonPath("$.app.mail.alert-recipient", matchProperty("alert.recipient")))
.andExpect(jsonPath("$.app.cors.allowed-origins", matchProperty("rest.cors.allowed-origins")))
.andExpect(jsonPath("$.app.ui.url", matchProperty("dspace.ui.url")))
.andExpect(jsonPath("$.java").exists());
}
private Matcher<?> matchProperty(String name) {
return is(configurationService.getProperty(name));
}
}

View File

@@ -3164,4 +3164,28 @@ public class RelationshipRestRepositoryIT extends AbstractEntityIntegrationTest
.andExpect(jsonPath("$.page.totalElements", is(2)));
}
@Test
public void findTheCreatedRelationshipTypeTest() throws Exception {
context.turnOffAuthorisationSystem();
Relationship relationship = RelationshipBuilder
.createRelationshipBuilder(context, author1, orgUnit1, isOrgUnitOfPersonRelationshipType).build();
context.restoreAuthSystemState();
Integer relationshipId = relationship.getID();
getClient().perform(get("/api/core/relationships/" + relationshipId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id", is(relationship.getID())))
.andExpect(jsonPath("$._embedded.relationships").doesNotExist())
.andExpect(jsonPath("$._links.relationshipType.href",
containsString("/api/core/relationships/" + relationshipId + "/relationshipType"))
);
String adminToken = getAuthToken(admin.getEmail(), password);
getClient(adminToken).perform(get("/api/core/relationships/" + relationshipId + "/relationshipType"))
.andExpect(status().isOk());
}
}

View File

@@ -205,7 +205,7 @@ public class SubmissionDefinitionsControllerIT extends AbstractControllerIntegra
// We expect the content type to be "application/hal+json;charset=UTF-8"
.andExpect(content().contentType(contentType))
// Match only that a section exists with a submission configuration behind
.andExpect(jsonPath("$._embedded.submissionsections", hasSize(7)))
.andExpect(jsonPath("$._embedded.submissionsections", hasSize(8)))
.andExpect(jsonPath("$._embedded.submissionsections",
Matchers.hasItem(
allOf(

View File

@@ -17,6 +17,7 @@ import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertTrue;
import static org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST_VALUE;
import static org.springframework.http.MediaType.parseMediaType;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
@@ -372,7 +373,9 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration
WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1)
.withTitle("Workspace Item 1")
.withIssueDate("2017-10-17")
.withAuthor("Smith, Donald").withAuthor("Doe, John")
.withAuthor("Smith, Donald")
.withIssn("222731-0582")
.withAuthor("Doe, John")
.withSubject("ExtraEntry")
.build();
@@ -7597,4 +7600,200 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration
.andExpect(jsonPath("$.page.totalElements", is(1)));
}
@Test
public void sherpaPolicySectionCacheTest() throws Exception {
context.turnOffAuthorisationSystem();
String dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX";
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.build();
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Collection 1")
.build();
WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1)
.withTitle("Workspace Item 1")
.withIssueDate("2021-11-21")
.withAuthor("Smith, Donald")
.withIssn("2731-0582")
.withSubject("ExtraEntry")
.build();
context.restoreAuthSystemState();
AtomicReference<String> retrievalTime = new AtomicReference<String>();
AtomicReference<String> retrievalTime2 = new AtomicReference<String>();
String token = getAuthToken(eperson.getEmail(), password);
getClient(token).perform(get("/api/submission/workspaceitems/" + witem.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.error", is(false)))))
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.journals[0].titles[0]",
is("Nature Synthesis")))))
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.journals[0].issns[0]",
is("2731-0582")))))
.andDo(result -> retrievalTime.set(read(
result.getResponse().getContentAsString(), "$.sections.sherpaPolicies.retrievalTime")));
Date date = new SimpleDateFormat(dateFormat).parse(retrievalTime.get());
// reload page, to verify that the retrievalTime is not changed
getClient(token).perform(get("/api/submission/workspaceitems/" + witem.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.error", is(false)))))
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.journals[0].titles[0]",
is("Nature Synthesis")))))
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.journals[0].issns[0]",
is("2731-0582")))))
.andDo(result -> retrievalTime2.set(read(
result.getResponse().getContentAsString(), "$.sections.sherpaPolicies.retrievalTime")));
Date date2 = new SimpleDateFormat(dateFormat).parse(retrievalTime2.get());
assertTrue(date.equals(date2));
// create a list of values to use in add operation
List<Operation> operations = new ArrayList<>();
operations.add(new RemoveOperation("/sections/sherpaPolicies/retrievalTime"));
// empty the cache and verify the retrivatTime
String patchBody = getPatchContent(operations);
getClient(token).perform(patch("/api/submission/workspaceitems/" + witem.getID())
.content(patchBody)
.contentType(MediaType.APPLICATION_JSON_PATCH_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.error", is(false)))))
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.journals[0].titles[0]",
is("Nature Synthesis")))))
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.journals[0].issns[0]",
is("2731-0582")))))
.andDo(result -> retrievalTime.set(read(
result.getResponse().getContentAsString(), "$.sections.sherpaPolicies.retrievalTime")));
date = new SimpleDateFormat(dateFormat).parse(retrievalTime.get());
assertTrue(date.after(date2));
// reload page, to verify that the retrievalTime is not changed
getClient(token).perform(get("/api/submission/workspaceitems/" + witem.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.error", is(false)))))
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.journals[0].titles[0]",
is("Nature Synthesis")))))
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.journals[0].issns[0]",
is("2731-0582")))))
.andDo(result -> retrievalTime2.set(read(
result.getResponse().getContentAsString(), "$.sections.sherpaPolicies.retrievalTime")));
date2 = new SimpleDateFormat(dateFormat).parse(retrievalTime2.get());
assertTrue(date.equals(date2));
}
@Test
public void sherpaPolicySectionWithWrongIssnCacheTest() throws Exception {
context.turnOffAuthorisationSystem();
String dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX";
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.build();
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Collection 1")
.build();
WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1)
.withTitle("Workspace Item 1")
.withIssueDate("2021-11-21")
.withAuthor("Smith, Donald")
.withIssn("0000-0000")
.withSubject("ExtraEntry")
.build();
context.restoreAuthSystemState();
AtomicReference<String> retrievalTime = new AtomicReference<String>();
AtomicReference<String> retrievalTime2 = new AtomicReference<String>();
String token = getAuthToken(eperson.getEmail(), password);
getClient(token).perform(get("/api/submission/workspaceitems/" + witem.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.error", is(true)))))
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.message", is("No results found")))))
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.journals", nullValue()))))
.andDo(result -> retrievalTime.set(read(
result.getResponse().getContentAsString(), "$.sections.sherpaPolicies.retrievalTime")));
Date date = new SimpleDateFormat(dateFormat).parse(retrievalTime.get());
// reload page, to verify that the retrievalTime is not changed
getClient(token).perform(get("/api/submission/workspaceitems/" + witem.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.error", is(true)))))
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.message", is("No results found")))))
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.journals", nullValue()))))
.andDo(result -> retrievalTime2.set(read(
result.getResponse().getContentAsString(), "$.sections.sherpaPolicies.retrievalTime")));
Date date2 = new SimpleDateFormat(dateFormat).parse(retrievalTime2.get());
assertTrue(date.equals(date2));
// create a list of values to use in add operation
List<Operation> operations = new ArrayList<>();
operations.add(new RemoveOperation("/sections/sherpaPolicies/retrievalTime"));
// empty the cache and verify the retrivatTime
String patchBody = getPatchContent(operations);
getClient(token).perform(patch("/api/submission/workspaceitems/" + witem.getID())
.content(patchBody)
.contentType(MediaType.APPLICATION_JSON_PATCH_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.error", is(true)))))
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.message", is("No results found")))))
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.journals", nullValue()))))
.andDo(result -> retrievalTime.set(read(
result.getResponse().getContentAsString(), "$.sections.sherpaPolicies.retrievalTime")));
date = new SimpleDateFormat(dateFormat).parse(retrievalTime.get());
assertTrue(date.after(date2));
// reload page, to verify that the retrievalTime is not changed
getClient(token).perform(get("/api/submission/workspaceitems/" + witem.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.error", is(true)))))
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.message", is("No results found")))))
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.sections.sherpaPolicies.sherpaResponse.journals", nullValue()))))
.andDo(result -> retrievalTime2.set(read(
result.getResponse().getContentAsString(), "$.sections.sherpaPolicies.retrievalTime")));
date2 = new SimpleDateFormat(dateFormat).parse(retrievalTime2.get());
assertTrue(date.equals(date2));
}
}

View File

@@ -0,0 +1,74 @@
/**
* 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.health;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.anEmptyMap;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.when;
import java.util.Map;
import com.maxmind.geoip2.DatabaseReader;
import org.dspace.app.rest.configuration.ActuatorConfiguration;
import org.dspace.statistics.GeoIpService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
/**
* Unit tests for {@link GeoIpHealthIndicator}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
@RunWith(MockitoJUnitRunner.class)
public class GeoIpHealthIndicatorTest {
@Mock
private GeoIpService geoIpService;
@InjectMocks
private GeoIpHealthIndicator geoIpHealthIndicator;
@Mock
private DatabaseReader databaseReader;
@Test
public void testWithGeoIpConfiguredCorrectly() {
when(geoIpService.getDatabaseReader()).thenReturn(databaseReader);
Health health = geoIpHealthIndicator.health();
assertThat(health.getStatus(), is(Status.UP));
assertThat(health.getDetails(), anEmptyMap());
}
@Test
public void testWithGeoIpWrongConfiguration() {
when(geoIpService.getDatabaseReader()).thenThrow(new IllegalStateException("Missing db file"));
Health health = geoIpHealthIndicator.health();
assertThat(health.getStatus(), is(ActuatorConfiguration.UP_WITH_ISSUES_STATUS));
assertThat(health.getDetails(), is(Map.of("reason", "Missing db file")));
}
@Test
public void testWithUnexpectedError() {
when(geoIpService.getDatabaseReader()).thenThrow(new RuntimeException("Generic error"));
Health health = geoIpHealthIndicator.health();
assertThat(health.getStatus(), is(Status.DOWN));
}
}

View File

@@ -0,0 +1,46 @@
/**
* 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.link.search;
import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.is;
import java.util.Map;
import org.hamcrest.Matcher;
import org.springframework.boot.actuate.health.Status;
/**
* Matcher for the health indicators.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public final class HealthIndicatorMatcher {
private HealthIndicatorMatcher() {
}
public static Matcher<? super Object> matchDatabase(Status status) {
return allOf(
hasJsonPath("$.db"),
hasJsonPath("$.db.status", is(status.getCode())),
hasJsonPath("$.db.components", allOf(
match("dspaceDataSource", status, Map.of("database", "H2", "validationQuery", "isValid()")),
match("dataSource", status, Map.of("database", "H2", "validationQuery", "isValid()")))));
}
public static Matcher<? super Object> match(String name, Status status, Map<String, Object> details) {
return allOf(
hasJsonPath("$." + name),
hasJsonPath("$." + name + ".status", is(status.getCode())),
hasJsonPath("$." + name + ".details", is(details)));
}
}

View File

@@ -1551,6 +1551,7 @@ module_dir = modules
# PRIOR to those below (and therefore may override configs in these default
# module configuration files).
include = ${module_dir}/actuator.cfg
include = ${module_dir}/altmetrics.cfg
include = ${module_dir}/authentication.cfg
include = ${module_dir}/authentication-ip.cfg

View File

@@ -1,22 +1,19 @@
<!--
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/
-->
<config
xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
<config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
xmlns='http://www.ehcache.org/v3'
xsi:schemaLocation="
http://www.ehcache.org/v3
http://www.ehcache.org/schema/ehcache-core-3.7.xsd">
xsi:schemaLocation="http://www.ehcache.org/v3
http://www.ehcache.org/schema/ehcache-core-3.7.xsd">
<cache-template name="iiif-default">
<listeners>
<listener>
<class>org.dspace.app.rest.cache.CacheLogger</class>
<class>org.dspace.iiif.logger.CacheLogger</class>
<event-firing-mode>ASYNCHRONOUS</event-firing-mode>
<event-ordering-mode>UNORDERED</event-ordering-mode>
<events-to-fire-on>CREATED</events-to-fire-on>
@@ -33,7 +30,7 @@
<cache-template name="iiif-canvas">
<listeners>
<listener>
<class>org.dspace.app.rest.cache.CanvasCacheLogger</class>
<class>org.dspace.iiif.logger.CanvasCacheLogger</class>
<event-firing-mode>ASYNCHRONOUS</event-firing-mode>
<event-ordering-mode>UNORDERED</event-ordering-mode>
<events-to-fire-on>CREATED</events-to-fire-on>
@@ -47,6 +44,26 @@
<offheap unit="MB">4</offheap>
</resources>
</cache-template>
<cache-template name="sherpa-default">
<listeners>
<listener>
<class>org.dspace.app.sherpa.cache.SherpaCacheLogger</class>
<event-firing-mode>ASYNCHRONOUS</event-firing-mode>
<event-ordering-mode>UNORDERED</event-ordering-mode>
<events-to-fire-on>CREATED</events-to-fire-on>
<events-to-fire-on>EXPIRED</events-to-fire-on>
<events-to-fire-on>REMOVED</events-to-fire-on>
<events-to-fire-on>EVICTED</events-to-fire-on>
</listener>
</listeners>
<resources>
<heap>3000</heap>
<offheap unit="MB">4</offheap>
</resources>
</cache-template>
<cache alias="manifests" uses-template="iiif-default"/>
<cache alias="canvasdimensions" uses-template="iiif-canvas"/>
</config>
<cache alias="sherpa.searchByJournalISSN" uses-template="sherpa-default"/>
</config>

View File

@@ -203,6 +203,12 @@
<processing-class>org.dspace.submit.step.SampleStep</processing-class>
<type>sample</type>
</step-definition>
<step-definition id="sherpaPolicies" mandatory="true">
<heading>submit.progressbar.sherpapolicy</heading>
<processing-class>org.dspace.app.rest.submit.step.SherpaPolicyStep</processing-class>
<type>sherpaPolicy</type>
</step-definition>
</step-definitions>
<!-- The submission-definitions map lays out the detailed definition of -->
@@ -235,8 +241,12 @@
<!-- Uncomment this step to allow the user to embargo or access restrict the entire item -->
<!-- <step id="itemAccessConditions"/> -->
<!-- Uncomment this step to show when appropriate publisher policies retrieved from SHERPA/RoMEO -->
<!-- <step id="sherpaPolicies"/> -->
<!--Step will be to Upload the item -->
<step id="upload"/>
<!-- <step id="extractionstep"/> -->
<!-- Uncomment this step to allow the user to select a Creative Commons License -->

View File

@@ -0,0 +1,58 @@
#---------------------------------------------------------------#
#------------SPRING BOOT ACTUATOR CONFIGURATION-----------------#
#---------------------------------------------------------------#
# Health endpoint configuration, for more details see https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints.health
## Configuration to establish when show the health status details
management.endpoint.health.show-details = when-authorized
## Configuration which users can see the health status details
management.endpoint.health.roles = ADMIN
## Configuration to establis
management.endpoint.health.status.order= down, out-of-service, up-with-issues, up, unknown
## Configuration that enables only health and info endpoints
management.endpoints.web.exposure.include=health,info
## Configuration to set 200 as status of health http response when it is DOWN or OUT_OF_SERVICE
## The DSpace UI requires these be set to 200 in order to support health status reports when services are down.
management.endpoint.health.status.http-mapping.down = 200
management.endpoint.health.status.http-mapping.out-of-service = 200
management.health.ping.enabled = false
management.health.diskSpace.enabled = false
# CORS configuration for all actuators
management.endpoints.web.cors.allowed-origins = ${rest.cors.allowed-origins}
management.endpoints.web.cors.allowed-methods = *
management.endpoints.web.cors.allowed-headers = Accept, Authorization, Content-Type, Origin, X-On-Behalf-Of, X-Requested-With, X-XSRF-TOKEN, X-CORRELATION-ID, X-REFERRER
management.endpoints.web.cors.exposed-headers = Authorization, DSPACE-XSRF-TOKEN, Location, WWW-Authenticate
management.endpoints.web.cors.allow-credentials = true
#---------------------------------------------------------------#
#------------------------INFO ENDPOINT--------------------------#
#---------------------------------------------------------------#
# All properties under the info key will be automatically exposed by the info actuator with a json structure. Furthermore, it is possible to
# enrich the content of the response given by the info actuator by defining in the Spring context beans of classes that implements InfoContributor.
management.info.env.enabled = true
management.info.java.enabled = true
info.app.name = ${dspace.name}
info.app.version = ${dspace.version}
info.app.dir = ${dspace.dir}
info.app.url = ${dspace.server.url}
info.app.db = ${db.url}
info.app.solr.server = ${solr.server}
info.app.solr.prefix = ${solr.multicorePrefix}
info.app.mail.server = ${mail.server}
info.app.mail.from-address = ${mail.from.address}
info.app.mail.feedback-recipient = ${feedback.recipient}
info.app.mail.mail-admin = ${mail.admin}
info.app.mail.mail-helpdesk = ${mail.helpdesk}
info.app.mail.alert-recipient = ${alert.recipient}
info.app.cors.allowed-origins = ${rest.cors.allowed-origins}
info.app.ui.url = ${dspace.ui.url}

View File

@@ -0,0 +1,21 @@
<?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:cache="http://www.springframework.org/schema/cache"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache.xsd">
<cache:annotation-driven />
<bean id="cacheManager" class="org.springframework.cache.jcache.JCacheCacheManager">
<property name="cacheManager">
<bean class="org.springframework.cache.jcache.JCacheManagerFactoryBean">
<property name="cacheManagerUri" value="file:${dspace.dir}/config/ehcache.xml"/>
</bean>
</property>
</bean>
</beans>

View File

@@ -33,4 +33,16 @@
</property>
</bean>
<bean class="org.dspace.app.sherpa.SHERPAService" id="org.dspace.app.sherpa.SHERPAService">
<property name="maxNumberOfTries" value="3"/>
<!-- the value of this property is in milliseconds -->
<property name="sleepBetweenTimeouts" value="2000"/>
<!-- the value of this property is in milliseconds -->
<property name="timeout" value="5000"/>
</bean>
<bean class="org.dspace.app.sherpa.cache.SherpaCacheEvictService">
<property name="sherpaSubmitService"
ref="org.dspace.app.sherpa.submit.SHERPASubmitService"/>
</bean>
</beans>

View File

@@ -31,5 +31,7 @@
<bean id="solrLoggerService" class="org.dspace.statistics.SolrLoggerServiceImpl" lazy-init="true"/>
<bean class="org.dspace.statistics.SolrStatisticsCore" autowire-candidate="true"/>
<bean class="org.dspace.statistics.GeoIpService" autowire-candidate="true"/>
</beans>