Merge remote-tracking branch 'origin/coar-notify-7' into coar-notify-7-part-two

# Conflicts:
#	dspace-api/src/main/java/org/dspace/app/ldn/service/LDNMessageService.java
#	dspace-api/src/main/java/org/dspace/app/ldn/service/impl/LDNMessageServiceImpl.java
This commit is contained in:
Stefano Maffei
2024-02-29 17:56:38 +01:00
677 changed files with 18014 additions and 19154 deletions

View File

@@ -21,11 +21,11 @@ jobs:
# Also specify version of Java to use (this can allow us to optionally run tests on multiple JDKs in future)
matrix:
include:
# NOTE: Unit Tests include deprecated REST API v6 (as it has unit tests)
# NOTE: Unit Tests include a retry for occasionally failing tests
# - surefire.rerunFailingTestsCount => try again for flakey tests, and keep track of/report on number of retries
- type: "Unit Tests"
java: 11
mvnflags: "-DskipUnitTests=false -Pdspace-rest -Dsurefire.rerunFailingTestsCount=2"
mvnflags: "-DskipUnitTests=false -Dsurefire.rerunFailingTestsCount=2"
resultsdir: "**/target/surefire-reports/**"
# NOTE: ITs skip all code validation checks, as they are already done by Unit Test job.
# - enforcer.skip => Skip maven-enforcer-plugin rules

View File

@@ -102,6 +102,8 @@ jobs:
build_id: dspace-solr
image_name: dspace/dspace-solr
dockerfile_path: ./dspace/src/main/docker/dspace-solr/Dockerfile
# Must pass solrconfigs to the Dockerfile so that it can find the required Solr config files
dockerfile_additional_contexts: 'solrconfigs=./dspace/solr/'
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }}

View File

@@ -24,6 +24,12 @@ on:
dockerfile_context:
required: false
type: string
default: '.'
# Optionally a list of "additional_contexts" to pass to Dockerfile. Defaults to empty
dockerfile_additional_contexts:
required: false
type: string
default: ''
# If Docker image should have additional tag flavor details (e.g. a suffix), it may be passed in.
tags_flavor:
required: false
@@ -123,7 +129,9 @@ jobs:
id: docker_build
uses: docker/build-push-action@v5
with:
context: ${{ inputs.dockerfile_context || '.' }}
build-contexts: |
${{ inputs.dockerfile_additional_contexts }}
context: ${{ inputs.dockerfile_context }}
file: ${{ inputs.dockerfile_path }}
platforms: ${{ matrix.arch }}
# For pull requests, we run the Docker build (to ensure no PR changes break the build),

View File

@@ -19,7 +19,7 @@ RUN mkdir /install \
USER dspace
# Copy the DSpace source code (from local machine) into the workdir (excluding .dockerignore contents)
ADD --chown=dspace . /app/
# Build DSpace (note: this build doesn't include the optional, deprecated "dspace-rest" webapp)
# Build DSpace
# Copy the dspace-installer directory to /install. Clean up the build to keep the docker image small
# Maven flags here ensure that we skip building test environment and skip all code verification checks.
# These flags speed up this compilation as much as reasonably possible.

View File

@@ -21,9 +21,9 @@ RUN mkdir /install \
USER dspace
# Copy the DSpace source code (from local machine) into the workdir (excluding .dockerignore contents)
ADD --chown=dspace . /app/
# Build DSpace (INCLUDING the optional, deprecated "dspace-rest" webapp)
# Build DSpace
# Copy the dspace-installer directory to /install. Clean up the build to keep the docker image small
RUN mvn --no-transfer-progress package -Pdspace-rest && \
RUN mvn --no-transfer-progress package && \
mv /app/dspace/target/${TARGET_DIR}/* /install && \
mvn clean
@@ -67,17 +67,10 @@ ENV CATALINA_OPTS=-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:800
# Link the DSpace 'server' webapp into Tomcat's webapps directory.
# This ensures that when we start Tomcat, it runs from /server path (e.g. http://localhost:8080/server/)
# Also link the v6.x (deprecated) REST API off the "/rest" path
RUN ln -s $DSPACE_INSTALL/webapps/server /usr/local/tomcat/webapps/server && \
ln -s $DSPACE_INSTALL/webapps/rest /usr/local/tomcat/webapps/rest
RUN ln -s $DSPACE_INSTALL/webapps/server /usr/local/tomcat/webapps/server
# If you wish to run "server" webapp off the ROOT path, then comment out the above RUN, and uncomment the below RUN.
# You also MUST update the 'dspace.server.url' configuration to match.
# Please note that server webapp should only run on one path at a time.
#RUN mv /usr/local/tomcat/webapps/ROOT /usr/local/tomcat/webapps/ROOT.bk && \
# ln -s $DSPACE_INSTALL/webapps/server /usr/local/tomcat/webapps/ROOT && \
# ln -s $DSPACE_INSTALL/webapps/rest /usr/local/tomcat/webapps/rest
# ln -s $DSPACE_INSTALL/webapps/server /usr/local/tomcat/webapps/ROOT
# Overwrite the v6.x (deprecated) REST API's web.xml, so that we can run it on HTTP (defaults to requiring HTTPS)
# WARNING: THIS IS OBVIOUSLY INSECURE. NEVER DO THIS IN PRODUCTION.
COPY dspace/src/main/docker/test/rest_web.xml $DSPACE_INSTALL/webapps/rest/WEB-INF/web.xml
RUN sed -i -e "s|\${dspace.dir}|$DSPACE_INSTALL|" $DSPACE_INSTALL/webapps/rest/WEB-INF/web.xml

View File

@@ -1,5 +1,10 @@
version: "3.7"
networks:
# Default to using network named 'dspacenet' from docker-compose.yml.
# Its full name will be prepended with the project name (e.g. "-p d7" means it will be named "d7_dspacenet")
default:
name: ${COMPOSE_PROJECT_NAME}_dspacenet
external: true
services:
dspace-cli:
image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-latest}"
@@ -26,13 +31,8 @@ services:
- ./dspace/config:/dspace/config
entrypoint: /dspace/bin/dspace
command: help
networks:
- dspacenet
tty: true
stdin_open: true
volumes:
assetstore:
networks:
dspacenet:

View File

@@ -36,7 +36,7 @@ services:
depends_on:
- dspacedb
networks:
dspacenet:
- dspacenet
ports:
- published: 8080
target: 8080
@@ -89,8 +89,10 @@ services:
container_name: dspacesolr
image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}"
build:
context: .
dockerfile: ./dspace/src/main/docker/dspace-solr/Dockerfile
context: ./dspace/src/main/docker/dspace-solr/
# Provide path to Solr configs necessary to build Docker image
additional_contexts:
solrconfigs: ./dspace/solr/
args:
SOLR_VERSION: "${SOLR_VER:-8.11}"
networks:
@@ -123,6 +125,8 @@ services:
cp -r /opt/solr/server/solr/configsets/statistics/* statistics
precreate-core qaevent /opt/solr/server/solr/configsets/qaevent
cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent
precreate-core suggestion /opt/solr/server/solr/configsets/suggestion
cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion
exec solr -f
volumes:
assetstore:

View File

@@ -379,22 +379,6 @@
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.13.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
<exclusions>
<!-- Later version provided by dspace-api -->
<exclusion>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
@@ -549,7 +533,7 @@
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<artifactId>hamcrest</artifactId>
<scope>test</scope>
</dependency>
<dependency>
@@ -641,7 +625,7 @@
<dependency>
<groupId>com.maxmind.geoip2</groupId>
<artifactId>geoip2</artifactId>
<version>2.11.0</version>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>org.apache.ant</groupId>
@@ -805,7 +789,7 @@
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>5.7.1</version>
<version>5.9</version>
</dependency>
<!-- Email templating -->
@@ -892,32 +876,32 @@
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-buffer</artifactId>
<version>4.1.94.Final</version>
<version>4.1.106.Final</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport</artifactId>
<version>4.1.94.Final</version>
<version>4.1.106.Final</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-unix-common</artifactId>
<version>4.1.94.Final</version>
<version>4.1.106.Final</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-common</artifactId>
<version>4.1.94.Final</version>
<version>4.1.106.Final</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-handler</artifactId>
<version>4.1.94.Final</version>
<version>4.1.106.Final</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec</artifactId>
<version>4.1.94.Final</version>
<version>4.1.106.Final</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
@@ -927,7 +911,7 @@
<dependency>
<groupId>org.xmlunit</groupId>
<artifactId>xmlunit-core</artifactId>
<version>2.8.0</version>
<version>2.9.1</version>
<scope>test</scope>
</dependency>
<dependency>

View File

@@ -116,6 +116,17 @@ public final class CreateAdministrator {
protected CreateAdministrator()
throws Exception {
context = new Context();
try {
context.getDBConfig();
} catch (NullPointerException npr) {
// if database is null, there is no point in continuing. Prior to this exception and catch,
// NullPointerException was thrown, that wasn't very helpful.
throw new IllegalStateException("Problem connecting to database. This " +
"indicates issue with either network or version (or possibly some other). " +
"If you are running this in docker-compose, please make sure dspace-cli was " +
"built from the same sources as running dspace container AND that they are in " +
"the same project/network.");
}
groupService = EPersonServiceFactory.getInstance().getGroupService();
ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
}

View File

@@ -7,7 +7,7 @@
*/
package org.dspace.app.ldn;
public enum QueueStatus {
public enum LDNMessageQueueStatus {
/**
* Resulting processing status of an LDN Message (aka queue management)

View File

@@ -8,7 +8,9 @@
package org.dspace.app.ldn;
/**
*
* Constants for LDN metadata fields
*
* @author Francesco Bacchelli (francesco.bacchelli at 4science.com)
*/
public final class LDNMetadataFields {

View File

@@ -7,7 +7,6 @@
*/
package org.dspace.app.ldn;
import java.io.IOException;
import java.sql.SQLException;
import org.apache.logging.log4j.Logger;
@@ -32,17 +31,23 @@ public class LDNQueueExtractor {
private LDNQueueExtractor() {
}
public static int extractMessageFromQueue() throws IOException, SQLException {
log.info("START LDNQueueExtractor.extractMessageFromQueue()");
/**
* invokes
* @see org.dspace.app.ldn.service.impl.LDNMessageServiceImpl#extractAndProcessMessageFromQueue(Context)
* to process the oldest ldn messages from the queue. An LdnMessage is processed when is routed to a
* @see org.dspace.app.ldn.processor.LDNProcessor
* Also a +1 is added to the ldnMessage entity
* @see org.dspace.app.ldn.LDNMessageEntity#getQueueAttempts()
* @return the number of processed ldnMessages.
* @throws SQLException
*/
public static int extractMessageFromQueue() throws SQLException {
Context context = new Context(Context.Mode.READ_WRITE);
int processed_messages = ldnMessageService.extractAndProcessMessageFromQueue(context);
if (processed_messages >= 0) {
if (processed_messages > 0) {
log.info("Processed Messages x" + processed_messages);
} else {
log.error("Errors happened during the extract operations. Check the log above!");
}
context.complete();
log.info("END LDNQueueExtractor.extractMessageFromQueue()");
return processed_messages;
}

View File

@@ -7,7 +7,6 @@
*/
package org.dspace.app.ldn;
import java.io.IOException;
import java.sql.SQLException;
import org.apache.logging.log4j.Logger;
@@ -32,17 +31,22 @@ public class LDNQueueTimeoutChecker {
private LDNQueueTimeoutChecker() {
}
public static int checkQueueMessageTimeout() throws IOException, SQLException {
log.info("START LDNQueueTimeoutChecker.checkQueueMessageTimeout()");
/**
* invokes
* @see org.dspace.app.ldn.service.impl.LDNMessageServiceImpl#checkQueueMessageTimeout(Context)
* to refresh the queue status of timed-out and in progressing status ldn messages:
* according to their attempts put them back in queue or set their status as failed if maxAttempts
* reached.
* @return the number of managed ldnMessages.
* @throws SQLException
*/
public static int checkQueueMessageTimeout() throws SQLException {
Context context = new Context(Context.Mode.READ_WRITE);
int fixed_messages = 0;
fixed_messages = ldnMessageService.checkQueueMessageTimeout(context);
if (fixed_messages >= 0) {
if (fixed_messages > 0) {
log.info("Managed Messages x" + fixed_messages);
} else {
log.error("Errors happened during the check operation. Check the log above!");
}
log.info("END LDNQueueTimeoutChecker.checkQueueMessageTimeout()");
context.complete();
return fixed_messages;
}

View File

@@ -32,11 +32,11 @@ public class LDNRouter {
*/
public LDNProcessor route(LDNMessageEntity ldnMessage) {
if (ldnMessage == null) {
log.warn("an null LDNMessage is received for routing!");
log.warn("A null LDNMessage was received and could not be routed.");
return null;
}
if (StringUtils.isEmpty(ldnMessage.getType())) {
log.warn("LDNMessage " + ldnMessage + " has no type!");
log.warn("LDNMessage " + ldnMessage + " was received. It has no type, so it couldn't be routed.");
return null;
}
Set<String> ldnMessageTypeSet = new HashSet<String>();

View File

@@ -1,36 +0,0 @@
/**
* 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.ldn;
import static java.nio.charset.Charset.defaultCharset;
import org.springframework.http.MediaType;
/**
*
*/
public class RdfMediaType {
public final static MediaType APPLICATION_JSON_LD = new MediaType("application", "ld+json", defaultCharset());
public final static MediaType APPLICATION_N_TRIPLES = new MediaType("application", "n-triples", defaultCharset());
public final static MediaType APPLICATION_RDF_XML = new MediaType("application", "rdf+xml", defaultCharset());
public final static MediaType APPLICATION_RDF_JSON = new MediaType("application", "rdf+json", defaultCharset());
public final static MediaType APPLICATION_X_TURTLE = new MediaType("application", "x-turtle", defaultCharset());
public final static MediaType TEXT_TURTLE = new MediaType("text", "turtle", defaultCharset());
public final static MediaType TEXT_N3 = new MediaType("text", "n3", defaultCharset());
public final static MediaType TEXT_RDF_N3 = new MediaType("text", "rdf+n3", defaultCharset());
/**
*
*/
private RdfMediaType() {
}
}

View File

@@ -26,6 +26,6 @@ public interface LDNAction {
* @return ActionStatus the resulting status of the action
* @throws Exception general exception that can be thrown while executing action
*/
public ActionStatus execute(Context context, Notification notification, Item item) throws Exception;
public LDNActionStatus execute(Context context, Notification notification, Item item) throws Exception;
}

View File

@@ -10,6 +10,6 @@ package org.dspace.app.ldn.action;
/**
* Resulting status of an execution of an action.
*/
public enum ActionStatus {
public enum LDNActionStatus {
CONTINUE, ABORT;
}

View File

@@ -11,7 +11,7 @@ import java.math.BigDecimal;
import java.sql.SQLException;
import java.util.Date;
import com.github.jsonldjava.utils.JsonUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.app.ldn.NotifyServiceEntity;
@@ -51,8 +51,8 @@ public class LDNCorrectionAction implements LDNAction {
private HandleService handleService;
@Override
public ActionStatus execute(Context context, Notification notification, Item item) throws Exception {
ActionStatus result = ActionStatus.ABORT;
public LDNActionStatus execute(Context context, Notification notification, Item item) throws Exception {
LDNActionStatus result = LDNActionStatus.ABORT;
String itemName = itemService.getName(item);
QAEvent qaEvent = null;
if (notification.getObject() != null) {
@@ -69,15 +69,14 @@ public class LDNCorrectionAction implements LDNAction {
}
BigDecimal score = getScore(context, notification);
double doubleScoreValue = score != null ? score.doubleValue() : 0d;
/* String fullHandleUrl = configurationService.getProperty("dspace.ui.url") + "/handle/"
+ handleService.findHandle(context, item); */
ObjectMapper mapper = new ObjectMapper();
qaEvent = new QAEvent(QAEvent.COAR_NOTIFY_SOURCE,
handleService.findHandle(context, item), item.getID().toString(), itemName,
this.getQaEventTopic(), doubleScoreValue,
JsonUtils.toString(message)
, new Date());
mapper.writeValueAsString(message),
new Date());
qaEventService.store(context, qaEvent);
result = ActionStatus.CONTINUE;
result = LDNActionStatus.CONTINUE;
}
return result;

View File

@@ -70,7 +70,7 @@ public class LDNEmailAction implements LDNAction {
* @throws Exception
*/
@Override
public ActionStatus execute(Context context, Notification notification, Item item) throws Exception {
public LDNActionStatus execute(Context context, Notification notification, Item item) throws Exception {
try {
Locale supportedLocale = I18nUtil.getEPersonLocale(context.getCurrentUser());
Email email = Email.getEmail(I18nUtil.getEmailFilename(supportedLocale, actionSendEmailTextFile));
@@ -85,7 +85,8 @@ public class LDNEmailAction implements LDNAction {
email.addArgument(notification.getActor().getName());
email.addArgument(item.getName());
email.addArgument(notification.getActor().getId());
email.addArgument(notification.getContext().getId());
email.addArgument(notification.getContext() != null ?
notification.getContext().getId() : notification.getObject().getId());
email.addArgument(item.getSubmitter().getFullName());
email.addArgument(date);
email.addArgument(notification);
@@ -96,7 +97,7 @@ public class LDNEmailAction implements LDNAction {
log.error("An Error Occurred while sending a notification email", e);
}
return ActionStatus.CONTINUE;
return LDNActionStatus.CONTINUE;
}
/**

View File

@@ -11,7 +11,7 @@ import java.math.BigDecimal;
import java.sql.SQLException;
import java.util.Date;
import com.github.jsonldjava.utils.JsonUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.app.ldn.NotifyServiceEntity;
@@ -51,14 +51,12 @@ public class LDNRelationCorrectionAction implements LDNAction {
private HandleService handleService;
@Override
public ActionStatus execute(Context context, Notification notification, Item item) throws Exception {
ActionStatus result = ActionStatus.ABORT;
public LDNActionStatus execute(Context context, Notification notification, Item item) throws Exception {
LDNActionStatus result = LDNActionStatus.ABORT;
String itemName = itemService.getName(item);
QAEvent qaEvent = null;
if (notification.getObject() != null) {
NotifyMessageDTO message = new NotifyMessageDTO();
/*relationFormat.replace("[0]", notification.getObject().getAsRelationship());
hrefValue = hrefValue.replace("[1]", notification.getObject().getAsSubject());*/
message.setHref(notification.getObject().getAsSubject());
message.setRelationship(notification.getObject().getAsRelationship());
if (notification.getOrigin() != null) {
@@ -67,13 +65,14 @@ public class LDNRelationCorrectionAction implements LDNAction {
}
BigDecimal score = getScore(context, notification);
double doubleScoreValue = score != null ? score.doubleValue() : 0d;
ObjectMapper mapper = new ObjectMapper();
qaEvent = new QAEvent(QAEvent.COAR_NOTIFY_SOURCE,
handleService.findHandle(context, item), item.getID().toString(), itemName,
this.getQaEventTopic(), doubleScoreValue,
JsonUtils.toString(message)
, new Date());
mapper.writeValueAsString(message),
new Date());
qaEventService.store(context, qaEvent);
result = ActionStatus.CONTINUE;
result = LDNActionStatus.CONTINUE;
}
return result;

View File

@@ -7,23 +7,23 @@
*/
package org.dspace.app.ldn.action;
import static org.dspace.app.ldn.RdfMediaType.APPLICATION_JSON_LD;
import java.util.ArrayList;
import java.util.List;
import java.net.URI;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.Header;
import org.apache.http.HttpException;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.app.ldn.converter.JsonLdHttpMessageConverter;
import org.dspace.app.ldn.model.Notification;
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
/**
* Action to send LDN Message
@@ -34,75 +34,85 @@ public class SendLDNMessageAction implements LDNAction {
private static final Logger log = LogManager.getLogger(SendLDNMessageAction.class);
private final RestTemplate restTemplate;
private CloseableHttpClient client = null;
public SendLDNMessageAction() {
restTemplate = new RestTemplate();
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
messageConverters.add(new JsonLdHttpMessageConverter());
messageConverters.addAll(restTemplate.getMessageConverters());
restTemplate.setMessageConverters(messageConverters);
HttpClientBuilder builder = HttpClientBuilder.create();
client = builder
.disableAutomaticRetries()
.setMaxConnTotal(5)
.build();
}
@Override
public ActionStatus execute(Context context, Notification notification, Item item) throws Exception {
//TODO authorization with Bearer token should be supported.
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", APPLICATION_JSON_LD.toString());
HttpEntity<Notification> request = new HttpEntity<>(notification, headers);
log.info("Announcing notification {}", request);
try {
//Server-side request forgery Critical check gitHub failure is a false positive,
//because the LDN Service URL is configured by the user from DSpace
//frontend configuration at /admin/ldn/services
ResponseEntity<String> response = restTemplate.postForEntity(
notification.getTarget().getInbox(),
request,
String.class);
if (isSuccessful(response.getStatusCode())) {
return ActionStatus.CONTINUE;
} else if (isRedirect(response.getStatusCode())) {
return handleRedirect(response, request);
} else {
return ActionStatus.ABORT;
}
} catch (Exception e) {
log.error(e);
return ActionStatus.ABORT;
public SendLDNMessageAction(CloseableHttpClient client) {
this();
if (client != null) {
this.client = client;
}
}
private boolean isSuccessful(HttpStatus statusCode) {
return statusCode == HttpStatus.ACCEPTED ||
statusCode == HttpStatus.CREATED;
@Override
public LDNActionStatus execute(Context context, Notification notification, Item item) throws Exception {
//TODO authorization with Bearer token should be supported.
String url = notification.getTarget().getInbox();
HttpPost httpPost = new HttpPost(url);
httpPost.addHeader("Content-Type", "application, ld+json");
ObjectMapper mapper = new ObjectMapper();
httpPost.setEntity(new StringEntity(mapper.writeValueAsString(notification), "UTF-8"));
LDNActionStatus result = LDNActionStatus.ABORT;
// NOTE: Github believes there is a "Potential server-side request forgery due to a user-provided value"
// This is a false positive because the LDN Service URL is configured by the user from DSpace.
// See the frontend configuration at [dspace.ui.url]/admin/ldn/services
try (
CloseableHttpResponse response = client.execute(httpPost);
) {
if (isSuccessful(response.getStatusLine().getStatusCode())) {
result = LDNActionStatus.CONTINUE;
} else if (isRedirect(response.getStatusLine().getStatusCode())) {
result = handleRedirect(response, httpPost);
}
} catch (Exception e) {
log.error(e);
}
return result;
}
private boolean isRedirect(HttpStatus statusCode) {
return statusCode == HttpStatus.PERMANENT_REDIRECT ||
statusCode == HttpStatus.TEMPORARY_REDIRECT;
private boolean isSuccessful(int statusCode) {
return statusCode == HttpStatus.SC_ACCEPTED ||
statusCode == HttpStatus.SC_CREATED;
}
private ActionStatus handleRedirect(ResponseEntity<String> response,
HttpEntity<Notification> request) {
String url = response.getHeaders().getFirst(HttpHeaders.LOCATION);
private boolean isRedirect(int statusCode) {
//org.apache.http.HttpStatus has no enum value for 308!
return statusCode == (HttpStatus.SC_TEMPORARY_REDIRECT + 1) ||
statusCode == HttpStatus.SC_TEMPORARY_REDIRECT;
}
private LDNActionStatus handleRedirect(CloseableHttpResponse oldresponse,
HttpPost request) throws HttpException {
Header[] urls = oldresponse.getHeaders(HttpHeaders.LOCATION);
String url = urls.length > 0 && urls[0] != null ? urls[0].getValue() : null;
if (url == null) {
throw new HttpException("Error following redirect, unable to reach"
+ " the correct url.");
}
LDNActionStatus result = LDNActionStatus.ABORT;
try {
ResponseEntity<String> responseEntity =
restTemplate.postForEntity(url, request, String.class);
if (isSuccessful(responseEntity.getStatusCode())) {
return ActionStatus.CONTINUE;
request.setURI(new URI(url));
try (
CloseableHttpResponse response = client.execute(request);
) {
if (isSuccessful(response.getStatusLine().getStatusCode())) {
return LDNActionStatus.CONTINUE;
}
}
} catch (Exception e) {
log.error("Error following redirect:", e);
}
return ActionStatus.ABORT;
return LDNActionStatus.ABORT;
}
}

View File

@@ -1,101 +0,0 @@
/**
* 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.ldn.converter;
import static org.dspace.app.ldn.RdfMediaType.APPLICATION_JSON_LD;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.github.jsonldjava.utils.JsonUtils;
import org.dspace.app.ldn.model.Notification;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
/**
* Message converter for JSON-LD notification request body.
*/
public class JsonLdHttpMessageConverter extends AbstractHttpMessageConverter<Notification> {
private final ObjectMapper objectMapper;
/**
* Initialize object mapper to normalize arrays on
* serialization/deserialization.
*/
public JsonLdHttpMessageConverter() {
super(APPLICATION_JSON_LD);
objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
objectMapper.enable(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED);
objectMapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
}
/**
* Instruct message converter to convert notification object.
*
* @param clazz current class pending conversion
* @return boolean whether to convert the object with this converter
*/
@Override
protected boolean supports(Class<?> clazz) {
return Notification.class.isAssignableFrom(clazz);
}
/**
* Convert input stream, primarily request body, to notification.
*
* @param clazz notification class
* @param inputMessage input message with body to convert
*
* @return Notification deserialized notification
*
* @throws IOException failed to convert input stream
* @throws HttpMessageNotReadableException something wrong with the input
* message
*/
@Override
protected Notification readInternal(Class<? extends Notification> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
try (InputStream in = inputMessage.getBody()) {
return objectMapper.convertValue(JsonUtils.fromInputStream(in), Notification.class);
}
}
/**
* Convert notification to output stream, primarily response body.
*
* @param notification notification to convert
* @param outputMessage output message with serialized notification
* @throws IOException failed to convert notification
* @throws HttpMessageNotWritableException something wrong with the output
* message
*/
@Override
protected void writeInternal(Notification notification, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
try (OutputStreamWriter out = new OutputStreamWriter(outputMessage.getBody())) {
JsonUtils.write(out, notification);
}
}
/**
* @return String
*/
public String getRdfType() {
return "JSON-LD";
}
}

View File

@@ -25,13 +25,44 @@ import org.dspace.core.GenericDAO;
*/
public interface LDNMessageDao extends GenericDAO<LDNMessageEntity> {
/**
* load the oldest ldn messages considering their {@link org.dspace.app.ldn.LDNMessageEntity#queueLastStartTime}
* @param context
* @param max_attempts consider ldn_message entity with queue_attempts <= max_attempts
* @return ldn message entities to be routed
* @throws SQLException
*/
public List<LDNMessageEntity> findOldestMessageToProcess(Context context, int max_attempts) throws SQLException;
/**
* find ldn message entties in processing status and already timed out.
* @param context
* @param max_attempts consider ldn_message entity with queue_attempts <= max_attempts
* @return ldn message entities
* @throws SQLException
*/
public List<LDNMessageEntity> findProcessingTimedoutMessages(Context context, int max_attempts) throws SQLException;
/**
* find all ldn messages related to an item
* @param context
* @param item item related to the returned ldn messages
* @param activities involves only this specific group of activities
* @return all ldn messages related to the given item
* @throws SQLException
*/
public List<LDNMessageEntity> findAllMessagesByItem(
Context context, Item item, String... activities) throws SQLException;
/**
* find all ldn messages related to an item and to a specific ldn message
* @param context
* @param msg the referring ldn message
* @param item the referring repository item
* @param relatedTypes filter for @see org.dspace.app.ldn.LDNMessageEntity#activityStreamType
* @return all related ldn messages
* @throws SQLException
*/
public List<LDNMessageEntity> findAllRelatedMessagesByItem(
Context context, LDNMessageEntity msg, Item item, String... relatedTypes) throws SQLException;

View File

@@ -39,7 +39,6 @@ public class LDNMessageDaoImpl extends AbstractHibernateDAO<LDNMessageEntity> im
@Override
public List<LDNMessageEntity> findOldestMessageToProcess(Context context, int max_attempts) throws SQLException {
// looking for oldest failed-processed message
CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
CriteriaQuery<LDNMessageEntity> criteriaQuery = getCriteriaQuery(criteriaBuilder, LDNMessageEntity.class);
Root<LDNMessageEntity> root = criteriaQuery.from(LDNMessageEntity.class);
@@ -54,7 +53,6 @@ public class LDNMessageDaoImpl extends AbstractHibernateDAO<LDNMessageEntity> im
orderList.add(criteriaBuilder.desc(root.get(LDNMessageEntity_.queueAttempts)));
orderList.add(criteriaBuilder.asc(root.get(LDNMessageEntity_.queueLastStartTime)));
criteriaQuery.orderBy(orderList);
// setHint("org.hibernate.cacheable", Boolean.FALSE);
List<LDNMessageEntity> result = list(context, criteriaQuery, false, LDNMessageEntity.class, -1, -1);
if (result == null || result.isEmpty()) {
log.debug("No LDN messages found to be processed");
@@ -102,7 +100,6 @@ public class LDNMessageDaoImpl extends AbstractHibernateDAO<LDNMessageEntity> im
orderList.add(criteriaBuilder.desc(root.get(LDNMessageEntity_.queueAttempts)));
orderList.add(criteriaBuilder.asc(root.get(LDNMessageEntity_.queueLastStartTime)));
criteriaQuery.orderBy(orderList);
// setHint("org.hibernate.cacheable", Boolean.FALSE);
List<LDNMessageEntity> result = list(context, criteriaQuery, false, LDNMessageEntity.class, -1, -1);
if (result == null || result.isEmpty()) {
log.debug("No LDN messages found to be processed");
@@ -121,8 +118,6 @@ public class LDNMessageDaoImpl extends AbstractHibernateDAO<LDNMessageEntity> im
Predicate relatedtypePredicate = null;
andPredicates.add(
criteriaBuilder.equal(root.get(LDNMessageEntity_.queueStatus), LDNMessageEntity.QUEUE_STATUS_PROCESSED));
/*andPredicates.add(
criteriaBuilder.equal(root.get(LDNMessageEntity_.object), item));*/
andPredicates.add(
criteriaBuilder.isNull(root.get(LDNMessageEntity_.target)));
andPredicates.add(
@@ -136,7 +131,6 @@ public class LDNMessageDaoImpl extends AbstractHibernateDAO<LDNMessageEntity> im
orderList.add(criteriaBuilder.asc(root.get(LDNMessageEntity_.queueLastStartTime)));
orderList.add(criteriaBuilder.desc(root.get(LDNMessageEntity_.queueAttempts)));
criteriaQuery.orderBy(orderList);
// setHint("org.hibernate.cacheable", Boolean.FALSE);
List<LDNMessageEntity> result = list(context, criteriaQuery, false, LDNMessageEntity.class, -1, -1);
if (result == null || result.isEmpty()) {
log.debug("No LDN messages ACK found to be processed");
@@ -157,8 +151,6 @@ public class LDNMessageDaoImpl extends AbstractHibernateDAO<LDNMessageEntity> im
criteriaBuilder.equal(root.get(LDNMessageEntity_.queueStatus), LDNMessageEntity.QUEUE_STATUS_PROCESSED));
andPredicates.add(
criteriaBuilder.equal(root.get(LDNMessageEntity_.object), item));
andPredicates.add(
criteriaBuilder.isNull(root.get(LDNMessageEntity_.origin)));
if (activities != null && activities.length > 0) {
activityPredicate = root.get(LDNMessageEntity_.activityStreamType).in(activities);
andPredicates.add(activityPredicate);
@@ -168,7 +160,6 @@ public class LDNMessageDaoImpl extends AbstractHibernateDAO<LDNMessageEntity> im
orderList.add(criteriaBuilder.asc(root.get(LDNMessageEntity_.queueLastStartTime)));
orderList.add(criteriaBuilder.desc(root.get(LDNMessageEntity_.queueAttempts)));
criteriaQuery.orderBy(orderList);
// setHint("org.hibernate.cacheable", Boolean.FALSE);
List<LDNMessageEntity> result = list(context, criteriaQuery, false, LDNMessageEntity.class, -1, -1);
if (result == null || result.isEmpty()) {
log.debug("No LDN messages found");

View File

@@ -10,7 +10,7 @@ package org.dspace.app.ldn.model;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
*
* used to map @see org.dspace.app.ldn.model.Notification
*/
public class Actor extends Base {

View File

@@ -17,7 +17,7 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
*
* used to map @see org.dspace.app.ldn.model.Notification
*/
@JsonInclude(Include.NON_EMPTY)
@JsonIgnoreProperties(ignoreUnknown = true)

View File

@@ -9,8 +9,9 @@ package org.dspace.app.ldn.model;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
*
* used to map @see org.dspace.app.ldn.model.Notification
*/
public class Citation extends Base {

View File

@@ -11,8 +11,9 @@ import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
*
* used to map @see org.dspace.app.ldn.model.Notification
*/
public class Context extends Citation {

View File

@@ -11,7 +11,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
/**
*
* the json object from witch @see org.dspace.app.ldn.LDNMessageEntity are created.
* <a href="https://notify.coar-repositories.org/patterns/">see official coar doc</a>
*/
@JsonPropertyOrder(value = {
"@context",

View File

@@ -10,7 +10,7 @@ package org.dspace.app.ldn.model;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
*
* used to map @see org.dspace.app.ldn.model.Notification
*/
public class Object extends Citation {

View File

@@ -10,7 +10,7 @@ package org.dspace.app.ldn.model;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
*
* used to map @see org.dspace.app.ldn.model.Notification
*/
public class Service extends Base {

View File

@@ -10,7 +10,7 @@ package org.dspace.app.ldn.model;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
*
* used to map @see org.dspace.app.ldn.model.Citation
*/
public class Url extends Base {

View File

@@ -50,7 +50,7 @@ public class LDNContextRepeater {
/**
* @param notification
* @return Iterator<Notification>
* @return Iterator<Notification>
*/
public Iterator<Notification> iterator(Notification notification) {
return new NotificationIterator(notification, repeatOver);
@@ -79,7 +79,7 @@ public class LDNContextRepeater {
JsonNode notificationNode = objectMapper.valueToTree(notification);
log.info("Notification {}", notificationNode);
log.debug("Notification {}", notificationNode);
JsonNode topContextNode = notificationNode.get(CONTEXT);
if (topContextNode.isNull()) {

View File

@@ -15,10 +15,12 @@ import java.util.List;
import java.util.Objects;
import java.util.UUID;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpResponseException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.app.ldn.action.ActionStatus;
import org.dspace.app.ldn.action.LDNAction;
import org.dspace.app.ldn.action.LDNActionStatus;
import org.dspace.app.ldn.model.Notification;
import org.dspace.app.ldn.utility.LDNUtils;
import org.dspace.content.DSpaceObject;
@@ -28,8 +30,6 @@ import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.handle.service.HandleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
/**
* Linked Data Notification metadata processor for consuming notifications. The
@@ -79,8 +79,8 @@ public class LDNMetadataProcessor implements LDNProcessor {
*
* @throws Exception failed execute the action
*/
private ActionStatus runActions(Context context, Notification notification, Item item) throws Exception {
ActionStatus operation = ActionStatus.CONTINUE;
private LDNActionStatus runActions(Context context, Notification notification, Item item) throws Exception {
LDNActionStatus operation = LDNActionStatus.CONTINUE;
for (LDNAction action : actions) {
log.info("Running action {} for notification {} {}",
action.getClass().getSimpleName(),
@@ -88,7 +88,7 @@ public class LDNMetadataProcessor implements LDNProcessor {
notification.getType());
operation = action.execute(context, notification, item);
if (operation == ActionStatus.ABORT) {
if (operation == LDNActionStatus.ABORT) {
break;
}
}
@@ -134,8 +134,9 @@ public class LDNMetadataProcessor implements LDNProcessor {
* @return Item associated item
*
* @throws SQLException failed to lookup item
* @throws HttpResponseException redirect failure
*/
private Item lookupItem(Context context, Notification notification) throws SQLException {
private Item lookupItem(Context context, Notification notification) throws SQLException, HttpResponseException {
Item item = null;
String url = null;
@@ -153,7 +154,7 @@ public class LDNMetadataProcessor implements LDNProcessor {
item = itemService.find(context, uuid);
if (Objects.isNull(item)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
throw new HttpResponseException(HttpStatus.SC_NOT_FOUND,
format("Item with uuid %s not found", uuid));
}
@@ -161,21 +162,21 @@ public class LDNMetadataProcessor implements LDNProcessor {
String handle = handleService.resolveUrlToHandle(context, url);
if (Objects.isNull(handle)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
throw new HttpResponseException(HttpStatus.SC_NOT_FOUND,
format("Handle not found for %s", url));
}
DSpaceObject object = handleService.resolveToObject(context, handle);
if (Objects.isNull(object)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
throw new HttpResponseException(HttpStatus.SC_NOT_FOUND,
format("Item with handle %s not found", handle));
}
if (object.getType() == Constants.ITEM) {
item = (Item) object;
} else {
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY,
throw new HttpResponseException(HttpStatus.SC_UNPROCESSABLE_ENTITY,
format("Handle %s does not resolve to an item", handle));
}
}

View File

@@ -128,6 +128,7 @@ public interface LDNMessageService {
* @throws SQLException If something goes wrong in the database
*/
public NotifyRequestStatus findRequestsByItem(Context context, Item item) throws SQLException;
/**
* delete the provided ldn message
*
@@ -144,4 +145,13 @@ public interface LDNMessageService {
* @throws SQLException if something goes wrong
*/
public List<LDNMessageEntity> findMessagesToBeReprocessed(Context context) throws SQLException;
/**
* check if IP number is included in the configured ip-range on the Notify
* Service
*
* @param origin the Notify Service entity
* @param sourceIp the ip to evaluate
*/
public boolean isValidIp(NotifyServiceEntity origin, String sourceIp);
}

View File

@@ -20,6 +20,7 @@ import java.util.UUID;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.JsonSyntaxException;
import org.apache.commons.lang.time.DateUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
@@ -93,6 +94,10 @@ public class LDNMessageServiceImpl implements LDNMessageService {
@Override
public LDNMessageEntity create(Context context, String id) throws SQLException {
LDNMessageEntity result = ldnMessageDao.findByID(context, LDNMessageEntity.class, id);
if (result != null) {
throw new SQLException("Duplicate LDN Message ID [" + id + "] detected. This message is rejected.");
}
return ldnMessageDao.create(context, new LDNMessageEntity(id));
}
@@ -104,7 +109,6 @@ public class LDNMessageServiceImpl implements LDNMessageService {
ldnMessage.setContext(findDspaceObjectByUrl(context, notification.getContext().getId()));
}
ldnMessage.setOrigin(findNotifyService(context, notification.getOrigin()));
ldnMessage.setTarget(findNotifyService(context, notification.getTarget()));
ldnMessage.setInReplyTo(find(context, notification.getInReplyTo()));
ObjectMapper mapper = new ObjectMapper();
String message = null;
@@ -130,38 +134,30 @@ public class LDNMessageServiceImpl implements LDNMessageService {
ldnMessage.setCoarNotifyType(notificationTypeArrayList.get(1));
}
ldnMessage.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_QUEUED);
//CST-12126 if source is untrusted, set the queue_status of the
//ldnMsgEntity to UNTRUSTED
ldnMessage.setSourceIp(sourceIp);
if (ldnMessage.getOrigin() == null && !"Offer".equalsIgnoreCase(ldnMessage.getActivityStreamType())) {
ldnMessage.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_UNTRUSTED);
} else {
boolean ipCheckRangeEnabled = configurationService.getBooleanProperty("ldn.ip-range.enabled", true);
if (ipCheckRangeEnabled && !isValidIp(ldnMessage.getOrigin(), sourceIp)) {
ldnMessage.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_UNTRUSTED_IP);
}
}
ldnMessage.setQueueTimeout(new Date());
ldnMessage.setSourceIp(sourceIp);
if (!isValidIp(ldnMessage)) {
ldnMessage.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_UNTRUSTED_IP);
}
update(context, ldnMessage);
return ldnMessage;
}
private boolean isValidIp(LDNMessageEntity message) {
@Override
public boolean isValidIp(NotifyServiceEntity origin, String sourceIp) {
boolean enabled = configurationService.getBooleanProperty("coar-notify.ip-range.enabled", true);
if (!enabled) {
return true;
}
NotifyServiceEntity notifyService =
message.getOrigin() == null ? message.getTarget() : message.getOrigin();
String lowerIp = notifyService.getLowerIp();
String upperIp = notifyService.getUpperIp();
String lowerIp = origin.getLowerIp();
String upperIp = origin.getUpperIp();
try {
InetAddress ip = InetAddress.getByName(message.getSourceIp());
InetAddress ip = InetAddress.getByName(sourceIp);
InetAddress lowerBoundAddress = InetAddress.getByName(lowerIp);
InetAddress upperBoundAddress = InetAddress.getByName(upperIp);
@@ -322,7 +318,7 @@ public class LDNMessageServiceImpl implements LDNMessageService {
}
log.debug("Using parameters: [timeoutInMinutes]=" + timeoutInMinutes + ",[maxAttempts]=" + maxAttempts);
/*
* CST-10631 put failed on processing messages with timed-out timeout and
* put failed on processing messages with timed-out timeout and
* attempts >= configured_max_attempts put queue on processing messages with
* timed-out timeout and attempts < configured_max_attempts
*/
@@ -335,7 +331,6 @@ public class LDNMessageServiceImpl implements LDNMessageService {
return result;
}
if (msgsToCheck == null || msgsToCheck.isEmpty()) {
log.info("No timedout LDN messages found in queue.");
return result;
}
for (int i = 0; i < msgsToCheck.size() && msgsToCheck.get(i) != null; i++) {
@@ -365,8 +360,8 @@ public class LDNMessageServiceImpl implements LDNMessageService {
if (msgs != null && !msgs.isEmpty()) {
for (LDNMessageEntity msg : msgs) {
RequestStatus offer = new RequestStatus();
offer.setServiceName(msg.getTarget() == null ? "Unknown Service" : msg.getTarget().getName());
offer.setServiceUrl(msg.getTarget() == null ? "" : msg.getTarget().getUrl());
offer.setServiceName(msg.getOrigin() == null ? "Unknown Service" : msg.getOrigin().getName());
offer.setServiceUrl(msg.getOrigin() == null ? "" : msg.getOrigin().getUrl());
offer.setOfferType(LDNUtils.getNotifyType(msg.getCoarNotifyType()));
List<LDNMessageEntity> acks = ldnMessageDao.findAllRelatedMessagesByItem(
context, msg, item, "Accept", "TentativeReject", "TentativeAccept", "Announce");

View File

@@ -129,7 +129,7 @@ public abstract class SolrSuggestionProvider implements SuggestionProvider {
}
/**
*
* check if the externalDataObject may have suggestion
* @param context
* @param externalDataObject
* @return true if the externalDataObject could be suggested by this provider

View File

@@ -10,18 +10,23 @@ package org.dspace.app.suggestion;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
import java.io.IOException;
import java.lang.reflect.Type;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrQuery.SortClause;
@@ -33,6 +38,7 @@ import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.FacetParams;
import org.dspace.content.Item;
import org.dspace.content.dto.MetadataValueDTO;
import org.dspace.content.service.ItemService;
@@ -41,6 +47,7 @@ import org.dspace.services.factory.DSpaceServicesFactory;
import org.dspace.util.UUIDUtils;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Service to deal with the local suggestion solr core used by the
* SolrSuggestionProvider(s)
@@ -50,6 +57,8 @@ import org.springframework.beans.factory.annotation.Autowired;
*/
public class SolrSuggestionStorageServiceImpl implements SolrSuggestionStorageService {
private static final Logger log = LogManager.getLogger(SolrSuggestionStorageServiceImpl.class);
protected SolrClient solrSuggestionClient;
@Autowired
@@ -73,9 +82,12 @@ public class SolrSuggestionStorageServiceImpl implements SolrSuggestionStorageSe
public void addSuggestion(Suggestion suggestion, boolean force, boolean commit)
throws SolrServerException, IOException {
if (force || !exist(suggestion)) {
Gson gson = new Gson();
ObjectMapper jsonMapper = new JsonMapper();
jsonMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
SolrInputDocument document = new SolrInputDocument();
document.addField(SOURCE, suggestion.getSource());
// suggestion id is written as concatenation of
// source + ":" + targetID + ":" + idPart (of externalDataObj)
String suggestionFullID = suggestion.getID();
document.addField(SUGGESTION_FULLID, suggestionFullID);
document.addField(SUGGESTION_ID, suggestionFullID.split(":", 3)[2]);
@@ -89,7 +101,7 @@ public class SolrSuggestionStorageServiceImpl implements SolrSuggestionStorageSe
document.addField(EXTERNAL_URI, suggestion.getExternalSourceUri());
document.addField(SCORE, suggestion.getScore());
document.addField(PROCESSED, false);
document.addField(EVIDENCES, gson.toJson(suggestion.getEvidences()));
document.addField(EVIDENCES, jsonMapper.writeValueAsString(suggestion.getEvidences()));
getSolr().add(document);
if (commit) {
getSolr().commit();
@@ -240,16 +252,13 @@ public class SolrSuggestionStorageServiceImpl implements SolrSuggestionStorageSe
solrQuery.setFacet(true);
solrQuery.setFacetMinCount(1);
solrQuery.addFacetField(TARGET_ID);
solrQuery.setFacetLimit((int) (pageSize + offset));
solrQuery.setParam(FacetParams.FACET_OFFSET, String.valueOf(offset));
solrQuery.setFacetLimit((int) (pageSize));
QueryResponse response = getSolr().query(solrQuery);
FacetField facetField = response.getFacetField(TARGET_ID);
List<SuggestionTarget> suggestionTargets = new ArrayList<SuggestionTarget>();
int idx = 0;
for (Count c : facetField.getValues()) {
if (idx < offset) {
idx++;
continue;
}
SuggestionTarget target = new SuggestionTarget();
target.setSource(source);
target.setTotal((int) c.getCount());
@@ -326,9 +335,14 @@ public class SolrSuggestionStorageServiceImpl implements SolrSuggestionStorageSe
}
}
String evidencesJson = (String) solrDoc.getFieldValue(EVIDENCES);
Type listType = new TypeToken<ArrayList<SuggestionEvidence>>() {
}.getType();
List<SuggestionEvidence> evidences = new Gson().fromJson(evidencesJson, listType);
ObjectMapper jsonMapper = new JsonMapper();
jsonMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
List<SuggestionEvidence> evidences = new LinkedList<SuggestionEvidence>();
try {
evidences = jsonMapper.readValue(evidencesJson, new TypeReference<List<SuggestionEvidence>>() {});
} catch (JsonProcessingException e) {
log.error(e);
}
suggestion.getEvidences().addAll(evidences);
return suggestion;
}

View File

@@ -14,26 +14,36 @@ import org.dspace.content.Item;
import org.dspace.content.dto.MetadataValueDTO;
/**
*
* This entity contains metadatas that should be added to the targeted Item
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*/
public class Suggestion {
/** id of the suggestion */
private String id;
/** the dc.title of the item */
private String display;
/** the external source name the suggestion comes from */
private String source;
/** external uri of the item */
private String externalSourceUri;
/** item targeted by this suggestion */
private Item target;
private List<SuggestionEvidence> evidences = new LinkedList<SuggestionEvidence>();
private List<MetadataValueDTO> metadata = new LinkedList<MetadataValueDTO>();
/** suggestion creation
* @param source name of the external source
* @param target the targeted item in repository
* @param idPart external item id, used mainly for suggestion @see #id creation
* */
public Suggestion(String source, Item target, String idPart) {
this.source = source;
this.target = target;

View File

@@ -8,16 +8,21 @@
package org.dspace.app.suggestion;
/**
*
* This DTO class is returned by an {@link org.dspace.app.suggestion.openaire.EvidenceScorer} to model the concept of
* an evidence / fact that has been used to evaluate the precision of a suggestion increasing or decreasing the score
* of the suggestion.
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*/
public class SuggestionEvidence {
/** name of the evidence */
private String name;
/** positive or negative value to influence the score of the suggestion */
private double score;
/** additional notes */
private String notes;
public SuggestionEvidence() {

View File

@@ -13,22 +13,42 @@ import java.util.UUID;
import org.dspace.core.Context;
import org.dspace.external.model.ExternalDataObject;
/**
*
* Interface for suggestion management like finding and counting.
* @see org.dspace.app.suggestion.SuggestionTarget
* @author Francesco Bacchelli (francesco.bacchelli at 4science.com)
*
*/
public interface SuggestionProvider {
/** find all suggestion targets
* @see org.dspace.app.suggestion.SuggestionTarget
* */
public List<SuggestionTarget> findAllTargets(Context context, int pageSize, long offset);
/** count all suggestion targets */
public long countAllTargets(Context context);
/** find a suggestion target by UUID */
public SuggestionTarget findTarget(Context context, UUID target);
/** find unprocessed suggestions (paged) by target UUID
* @see org.dspace.app.suggestion.Suggestion
* */
public List<Suggestion> findAllUnprocessedSuggestions(Context context, UUID target, int pageSize, long offset,
boolean ascending);
/** find unprocessed suggestions by target UUID */
public long countUnprocessedSuggestionByTarget(Context context, UUID target);
/** find an unprocessed suggestion by target UUID and suggestion id */
public Suggestion findUnprocessedSuggestion(Context context, UUID target, String id);
/** reject a specific suggestion by target @param target and by suggestion id @param idPart */
public void rejectSuggestion(Context context, UUID target, String idPart);
/** flag a suggestion as processed */
public void flagRelatedSuggestionsAsProcessed(Context context, ExternalDataObject externalDataObject);
}

View File

@@ -13,36 +13,49 @@ import java.util.UUID;
import org.dspace.core.Context;
/**
*
* Service that handles {@link Suggestion}.
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*/
public interface SuggestionService {
/** find a {@link SuggestionTarget } by source name and suggestion id */
public SuggestionTarget find(Context context, String source, UUID id);
/** count all suggetion targets by suggestion source */
public long countAll(Context context, String source);
/** find all suggestion targets by source (paged) */
public List<SuggestionTarget> findAllTargets(Context context, String source, int pageSize, long offset);
/** count all (unprocessed) suggestions by the given target uuid */
public long countAllByTarget(Context context, UUID target);
/** find suggestion target by targeted item (paged) */
public List<SuggestionTarget> findByTarget(Context context, UUID target, int pageSize, long offset);
/** find suggestion source by source name */
public SuggestionSource findSource(Context context, String source);
/** count all suggestion sources */
public long countSources(Context context);
/** find all suggestion sources (paged) */
public List<SuggestionSource> findAllSources(Context context, int pageSize, long offset);
/** find unprocessed suggestion by id */
public Suggestion findUnprocessedSuggestion(Context context, String id);
/** reject a specific suggestion by its id */
public void rejectSuggestion(Context context, String id);
/** find all suggestions by targeted item and external source */
public List<Suggestion> findByTargetAndSource(Context context, UUID target, String source, int pageSize,
long offset, boolean ascending);
/** count all suggestions by targeted item id and source name */
public long countAllByTargetAndSource(Context context, String source, UUID target);
/** returns all suggestion providers */
public List<SuggestionProvider> getSuggestionProviders();
}

View File

@@ -76,6 +76,7 @@ public class SuggestionServiceImpl implements SuggestionService {
public List<SuggestionTarget> findByTarget(Context context, UUID target, int pageSize, long offset) {
List<SuggestionTarget> fullSourceTargets = new ArrayList<SuggestionTarget>();
for (String source : providersMap.keySet()) {
// all the suggestion target will be related to the same target (i.e. the same researcher - person item)
SuggestionTarget sTarget = providersMap.get(source).findTarget(context, target);
if (sTarget != null && sTarget.getTotal() > 0) {
fullSourceTargets.add(sTarget);
@@ -88,6 +89,8 @@ public class SuggestionServiceImpl implements SuggestionService {
}
}
);
// this list will be as large as the number of sources available in the repository so it is unlikely that
// real pagination will occur
return fullSourceTargets.stream().skip(offset).limit(pageSize).collect(Collectors.toList());
}

View File

@@ -8,14 +8,17 @@
package org.dspace.app.suggestion;
/**
*
* This DTO class is used to pass around the number of items interested by suggestion provided by a specific source
* (i.e. openaire)
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*/
public class SuggestionSource {
/** source name of the suggestion */
private String name;
/** number of targeted items */
private int total;
public SuggestionSource() {

View File

@@ -10,23 +10,27 @@ package org.dspace.app.suggestion;
import org.dspace.content.Item;
/**
*
* This DTO class is used to pass around the number of suggestions available from a specific source for a target
* repository item
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*/
public class SuggestionTarget {
/** the item targeted */
private Item target;
/** source name of the suggestion */
private String source;
/** total count of suggestions for same target and source */
private int total;
public SuggestionTarget() {
}
/**
* Wrap a target person into a suggestion target.
* Wrap a target repository item (usually a person item) into a suggestion target.
*
* @param item must be not null
*/
@@ -41,11 +45,7 @@ public class SuggestionTarget {
* @return the source:uuid of the wrapped item
*/
public String getID() {
if (target != null) {
return source + ":" + target.getID();
} else {
return source + ":null";
}
return source + ":" + (target != null ? target.getID() : "");
}
public Item getTarget() {

View File

@@ -5,7 +5,7 @@
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion.oaire;
package org.dspace.app.suggestion.openaire;
import static org.dspace.app.suggestion.SuggestionUtils.getAllEntriesByMetadatum;
@@ -130,6 +130,11 @@ public class AuthorNamesScorer implements EvidenceScorer {
return authors;
}
/**
* cleans up undesired characters
* @param value the string to clean up
* @return cleaned up string
* */
private String normalize(String value) {
String norm = Normalizer.normalize(value, Normalizer.NFD);
CharsetDetector cd = new CharsetDetector();

View File

@@ -5,7 +5,7 @@
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion.oaire;
package org.dspace.app.suggestion.openaire;
import java.util.Calendar;
import java.util.Collections;
@@ -31,23 +31,50 @@ import org.springframework.beans.factory.annotation.Autowired;
*/
public class DateScorer implements EvidenceScorer {
/**
* if available it should contains the metadata field key in the form (schema.element[.qualifier]) that contains
* the birth date of the researcher
*/
private String birthDateMetadata;
/**
* if available it should contains the metadata field key in the form (schema.element[.qualifier]) that contains
* the date of graduation of the researcher. If the metadata has multiple values the min will be used
*/
private String educationDateMetadata;
private String minDateMetadata;
private String maxDateMetadata;
/**
* The minimal age that is expected for a researcher to be a potential author of a scholarly contribution
* (i.e. the minimum delta from the publication date and the birth date)
*/
private int birthDateDelta = 20;
/**
* The maximum age that is expected for a researcher to be a potential author of a scholarly contribution
* (i.e. the maximum delta from the publication date and the birth date)
*/
private int birthDateRange = 50;
/**
* The number of year from/before the graduation that is expected for a researcher to be a potential
* author of a scholarly contribution (i.e. the minimum delta from the publication date and the first
* graduation date)
*/
private int educationDateDelta = -3;
/**
* The maximum scientific longevity that is expected for a researcher from its graduation to be a potential
* author of a scholarly contribution (i.e. the maximum delta from the publication date and the first
* graduation date)
*/
private int educationDateRange = 50;
@Autowired
private ItemService itemService;
/**
* the metadata used in the publication to track the publication date (i.e. dc.date.issued)
*/
private String publicationDateMetadata;
public void setItemService(ItemService itemService) {
@@ -86,14 +113,6 @@ public class DateScorer implements EvidenceScorer {
this.educationDateRange = educationDateRange;
}
public void setMaxDateMetadata(String maxDateMetadata) {
this.maxDateMetadata = maxDateMetadata;
}
public void setMinDateMetadata(String minDateMetadata) {
this.minDateMetadata = minDateMetadata;
}
public void setPublicationDateMetadata(String publicationDateMetadata) {
this.publicationDateMetadata = publicationDateMetadata;
}
@@ -101,6 +120,7 @@ public class DateScorer implements EvidenceScorer {
/**
* Method which is responsible to evaluate ImportRecord based on the publication date.
* ImportRecords which have a date outside the defined or calculated expected range will be discarded.
* {@link DateScorer#birthDateMetadata}, {@link DateScorer#educationDateMetadata}
*
* @param importRecord the ExternalDataObject to check
* @param researcher DSpace item
@@ -113,8 +133,7 @@ public class DateScorer implements EvidenceScorer {
return new SuggestionEvidence(this.getClass().getSimpleName(),
0,
"No assumption was possible about the publication year range. "
+ "Please consider to set a min/max date in the profile, specify the birthday "
+ "or education achievements");
+ "Please consider setting your birthday in your profile.");
} else {
String optDate = SuggestionUtils.getFirstEntryByMetadatum(importRecord, publicationDateMetadata);
int year = getYear(optDate);
@@ -139,34 +158,29 @@ public class DateScorer implements EvidenceScorer {
}
}
/**
* returns min and max year interval in between it's probably that the researcher
* actually contributed to the suggested item
* @param researcher
* @return
*/
private Integer[] calculateRange(Item researcher) {
String minDateStr = getSingleValue(researcher, minDateMetadata);
int minYear = getYear(minDateStr);
String maxDateStr = getSingleValue(researcher, maxDateMetadata);
int maxYear = getYear(maxDateStr);
if (minYear > 0 && maxYear > 0) {
return new Integer[] { minYear, maxYear };
String birthDateStr = getSingleValue(researcher, birthDateMetadata);
int birthDateYear = getYear(birthDateStr);
int educationDateYear = getListMetadataValues(researcher, educationDateMetadata).stream()
.mapToInt(x -> getYear(x.getValue())).filter(d -> d > 0).min().orElse(-1);
if (educationDateYear > 0) {
return new Integer[] {
educationDateYear + educationDateDelta,
educationDateYear + educationDateDelta + educationDateRange
};
} else if (birthDateYear > 0) {
return new Integer[] {
birthDateYear + birthDateDelta,
birthDateYear + birthDateDelta + birthDateRange
};
} else {
String birthDateStr = getSingleValue(researcher, birthDateMetadata);
int birthDateYear = getYear(birthDateStr);
int educationDateYear = getListMetadataValues(researcher, educationDateMetadata)
.stream()
.mapToInt(x -> getYear(x.getValue()))
.filter(d -> d > 0)
.min().orElse(-1);
if (educationDateYear > 0) {
return new Integer[] {
minYear > 0 ? minYear : educationDateYear + educationDateDelta,
maxYear > 0 ? maxYear : educationDateYear + educationDateDelta + educationDateRange
};
} else if (birthDateYear > 0) {
return new Integer[] {
minYear > 0 ? minYear : birthDateYear + birthDateDelta,
maxYear > 0 ? maxYear : birthDateYear + birthDateDelta + birthDateRange
};
} else {
return null;
}
return null;
}
}

View File

@@ -5,14 +5,14 @@
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion.oaire;
package org.dspace.app.suggestion.openaire;
import org.dspace.app.suggestion.SuggestionEvidence;
import org.dspace.content.Item;
import org.dspace.external.model.ExternalDataObject;
/**
* Interface used in {@see org.dspace.app.suggestion.oaire.OAIREPublicationApproverServiceImpl}
* Interface used in {@see org.dspace.app.suggestion.oaire.PublicationApproverServiceImpl}
* to construct filtering pipeline.
*
* For each EvidenceScorer, the service call computeEvidence method.

View File

@@ -5,7 +5,7 @@
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion.oaire;
package org.dspace.app.suggestion.openaire;
import static org.dspace.app.suggestion.SuggestionUtils.getAllEntriesByMetadatum;
import static org.dspace.app.suggestion.SuggestionUtils.getFirstEntryByMetadatum;
@@ -33,7 +33,7 @@ import org.springframework.beans.factory.annotation.Autowired;
* @author Pasquale Cavallo (pasquale.cavallo at 4science dot it)
*
*/
public class OAIREPublicationLoader extends SolrSuggestionProvider {
public class PublicationLoader extends SolrSuggestionProvider {
private List<String> names;
@@ -66,7 +66,7 @@ public class OAIREPublicationLoader extends SolrSuggestionProvider {
* This method filter a list of ImportRecords using a pipeline of AuthorNamesApprover
* and return a filtered list of ImportRecords.
*
* @see org.dspace.app.suggestion.oaire.AuthorNamesScorer
* @see org.dspace.app.suggestion.openaire.AuthorNamesScorer
* @param researcher the researcher Item
* @param importRecords List of import record
* @return a list of filtered import records
@@ -105,10 +105,22 @@ public class OAIREPublicationLoader extends SolrSuggestionProvider {
*/
public void importAuthorRecords(Context context, Item researcher)
throws SolrServerException, IOException {
List<ExternalDataObject> metadata = getImportRecords(researcher);
List<Suggestion> records = reduceAndTransform(researcher, metadata);
for (Suggestion record : records) {
solrSuggestionStorageService.addSuggestion(record, false, false);
int offset = 0;
int limit = 10;
int loaded = limit;
List<String> searchValues = searchMetadataValues(researcher);
while (loaded > 0) {
List<ExternalDataObject> metadata = getImportRecords(searchValues, researcher, offset, limit);
if (metadata.isEmpty()) {
loaded = 0;
continue;
}
offset += limit;
loaded = metadata.size();
List<Suggestion> records = reduceAndTransform(researcher, metadata);
for (Suggestion record : records) {
solrSuggestionStorageService.addSuggestion(record, false, false);
}
}
solrSuggestionStorageService.commit();
}
@@ -154,14 +166,18 @@ public class OAIREPublicationLoader extends SolrSuggestionProvider {
* get from metadata key defined in class level variable names as author to query OpenAIRE.
*
* @see org.dspace.importer.external.openaire.service.OpenAireImportMetadataSourceServiceImpl
* @param searchValues query
* @param researcher item to extract metadata from
* @param limit for pagination purpose
* @param offset for pagination purpose
* @return list of ImportRecord
*/
private List<ExternalDataObject> getImportRecords(Item researcher) {
List<String> searchValues = searchMetadataValues(researcher);
private List<ExternalDataObject> getImportRecords(List<String> searchValues,
Item researcher, int offset, int limit) {
List<ExternalDataObject> matchingRecords = new ArrayList<>();
for (String searchValue : searchValues) {
matchingRecords.addAll(primaryProvider.searchExternalDataObjects(searchValue, 0, 9999));
matchingRecords.addAll(
primaryProvider.searchExternalDataObjects(searchValue, offset, limit));
}
List<ExternalDataObject> toReturn = removeDuplicates(matchingRecords);
return toReturn;

View File

@@ -5,17 +5,17 @@
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion;
package org.dspace.app.suggestion.openaire;
import org.apache.commons.cli.Options;
/**
* Extension of {@link OAIREPublicationLoaderScriptConfiguration} for CLI.
* Extension of {@link PublicationLoaderScriptConfiguration} for CLI.
*
* @author Alessandro Martelli (alessandro.martelli at 4science.it)
*/
public class OAIREPublicationLoaderCliScriptConfiguration<T extends OAIREPublicationLoaderRunnable>
extends OAIREPublicationLoaderScriptConfiguration<T> {
public class PublicationLoaderCliScriptConfiguration<T extends PublicationLoaderRunnable>
extends PublicationLoaderScriptConfiguration<T> {
@Override
public Options getOptions() {

View File

@@ -5,19 +5,23 @@
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion;
package org.dspace.app.suggestion.openaire;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import org.apache.commons.cli.ParseException;
import org.dspace.app.suggestion.oaire.OAIREPublicationLoader;
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.dspace.discovery.IndexableObject;
import org.dspace.discovery.DiscoverQuery;
import org.dspace.discovery.SearchService;
import org.dspace.discovery.SearchServiceException;
import org.dspace.discovery.SearchUtils;
import org.dspace.discovery.utils.DiscoverQueryBuilder;
import org.dspace.discovery.utils.parameter.QueryBuilderSearchFilter;
import org.dspace.scripts.DSpaceRunnable;
import org.dspace.sort.SortOption;
import org.dspace.utils.DSpace;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -32,12 +36,12 @@ import org.slf4j.LoggerFactory;
*
* @author Alessandro Martelli (alessandro.martelli at 4science.it)
*/
public class OAIREPublicationLoaderRunnable
extends DSpaceRunnable<OAIREPublicationLoaderScriptConfiguration<OAIREPublicationLoaderRunnable>> {
public class PublicationLoaderRunnable
extends DSpaceRunnable<PublicationLoaderScriptConfiguration<PublicationLoaderRunnable>> {
private static final Logger LOGGER = LoggerFactory.getLogger(OAIREPublicationLoaderRunnable.class);
private static final Logger LOGGER = LoggerFactory.getLogger(PublicationLoaderRunnable.class);
private OAIREPublicationLoader oairePublicationLoader = null;
private PublicationLoader oairePublicationLoader = null;
protected Context context;
@@ -45,9 +49,9 @@ public class OAIREPublicationLoaderRunnable
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public OAIREPublicationLoaderScriptConfiguration<OAIREPublicationLoaderRunnable> getScriptConfiguration() {
OAIREPublicationLoaderScriptConfiguration configuration = new DSpace().getServiceManager()
.getServiceByName("import-oaire-suggestions", OAIREPublicationLoaderScriptConfiguration.class);
public PublicationLoaderScriptConfiguration<PublicationLoaderRunnable> getScriptConfiguration() {
PublicationLoaderScriptConfiguration configuration = new DSpace().getServiceManager()
.getServiceByName("import-openaire-suggestions", PublicationLoaderScriptConfiguration.class);
return configuration;
}
@@ -55,7 +59,7 @@ public class OAIREPublicationLoaderRunnable
public void setup() throws ParseException {
oairePublicationLoader = new DSpace().getServiceManager().getServiceByName(
"OAIREPublicationLoader", OAIREPublicationLoader.class);
"OpenairePublicationLoader", PublicationLoader.class);
profile = commandLine.getOptionValue("s");
if (profile == null) {
@@ -70,10 +74,9 @@ public class OAIREPublicationLoaderRunnable
context = new Context();
List<Item> researchers = getResearchers(profile);
for (Item researcher : researchers) {
Iterator<Item> researchers = getResearchers(profile);
while (researchers.hasNext()) {
Item researcher = researchers.next();
oairePublicationLoader.importAuthorRecords(context, researcher);
}
@@ -90,24 +93,23 @@ public class OAIREPublicationLoaderRunnable
* @return the researcher with specified UUID or all researchers
*/
@SuppressWarnings("rawtypes")
private List<Item> getResearchers(String profileUUID) {
final UUID uuid = profileUUID != null ? UUID.fromString(profileUUID) : null;
private Iterator<Item> getResearchers(String profileUUID) {
SearchService searchService = new DSpace().getSingletonService(SearchService.class);
List<IndexableObject> objects = null;
if (uuid != null) {
objects = searchService.search(context, "search.resourceid:" + uuid.toString(),
"lastModified", false, 0, 1000, "search.resourcetype:Item", "dspace.entity.type:Person");
} else {
objects = searchService.search(context, "*:*", "lastModified", false, 0, 1000, "search.resourcetype:Item",
"dspace.entity.type:Person");
DiscoverQueryBuilder queryBuilder = SearchUtils.getQueryBuilder();
List<QueryBuilderSearchFilter> filters = new ArrayList<QueryBuilderSearchFilter>();
String query = "*:*";
if (profileUUID != null) {
query = "search.resourceid:" + profileUUID.toString();
}
List<Item> items = new ArrayList<Item>();
if (objects != null) {
for (IndexableObject o : objects) {
items.add((Item) o.getIndexedObject());
}
try {
DiscoverQuery discoverQuery = queryBuilder.buildQuery(context, null,
SearchUtils.getDiscoveryConfigurationByName("person"),
query, filters,
"Item", 10, Long.getLong("0"), null, SortOption.DESCENDING);
return searchService.iteratorSearch(context, null, discoverQuery);
} catch (SearchServiceException e) {
LOGGER.error("Unable to read researcher on solr", e);
}
LOGGER.info("Found " + items.size() + " researcher(s)");
return items;
return null;
}
}

View File

@@ -5,19 +5,19 @@
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion;
package org.dspace.app.suggestion.openaire;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.ParseException;
import org.dspace.utils.DSpace;
public class OAIREPublicationLoaderRunnableCli extends OAIREPublicationLoaderRunnable {
public class PublicationLoaderRunnableCli extends PublicationLoaderRunnable {
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public OAIREPublicationLoaderCliScriptConfiguration getScriptConfiguration() {
OAIREPublicationLoaderCliScriptConfiguration configuration = new DSpace().getServiceManager()
.getServiceByName("import-oaire-suggestions", OAIREPublicationLoaderCliScriptConfiguration.class);
public PublicationLoaderCliScriptConfiguration getScriptConfiguration() {
PublicationLoaderCliScriptConfiguration configuration = new DSpace().getServiceManager()
.getServiceByName("import-openaire-suggestions", PublicationLoaderCliScriptConfiguration.class);
return configuration;
}
@@ -28,7 +28,7 @@ public class OAIREPublicationLoaderRunnableCli extends OAIREPublicationLoaderRun
// in case of CLI we show the help prompt
if (commandLine.hasOption('h')) {
HelpFormatter formatter = new HelpFormatter();
formatter.printHelp("Import Readearchers Suggestions", getScriptConfiguration().getOptions());
formatter.printHelp("Import Researchers Suggestions", getScriptConfiguration().getOptions());
System.exit(0);
}
}

View File

@@ -5,12 +5,12 @@
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion;
package org.dspace.app.suggestion.openaire;
import org.apache.commons.cli.Options;
import org.dspace.scripts.configuration.ScriptConfiguration;
public class OAIREPublicationLoaderScriptConfiguration<T extends OAIREPublicationLoaderRunnable>
public class PublicationLoaderScriptConfiguration<T extends PublicationLoaderRunnable>
extends ScriptConfiguration<T> {
private Class<T> dspaceRunnableClass;
@@ -22,7 +22,7 @@ public class OAIREPublicationLoaderScriptConfiguration<T extends OAIREPublicatio
/**
* Generic setter for the dspaceRunnableClass
* @param dspaceRunnableClass The dspaceRunnableClass to be set on this OAIREPublicationLoaderScriptConfiguration
* @param dspaceRunnableClass The dspaceRunnableClass to be set on this PublicationLoaderScriptConfiguration
*/
@Override
public void setDspaceRunnableClass(Class<T> dspaceRunnableClass) {

View File

@@ -628,12 +628,23 @@ public class AuthorizeUtil {
// actually expected to be returning true.
// For example the LDAP canSelfRegister will return true due to auto-register, while that
// does not imply a new user can register explicitly
return AuthenticateServiceFactory.getInstance().getAuthenticationService()
.allowSetPassword(context, request, null);
return authorizePasswordChange(context, request);
}
return false;
}
/**
* This method will return a boolean indicating whether the current user is allowed to reset the password
* or not
*
* @return A boolean indicating whether the current user can reset its password or not
* @throws SQLException If something goes wrong
*/
public static boolean authorizeForgotPassword() {
return DSpaceServicesFactory.getInstance().getConfigurationService()
.getBooleanProperty("user.forgot-password", true);
}
/**
* This method will return a boolean indicating whether it's allowed to update the password for the EPerson
* with the given email and canLogin property
@@ -647,8 +658,7 @@ public class AuthorizeUtil {
if (eperson != null && eperson.canLogIn()) {
HttpServletRequest request = new DSpace().getRequestService().getCurrentRequest()
.getHttpServletRequest();
return AuthenticateServiceFactory.getInstance().getAuthenticationService()
.allowSetPassword(context, request, null);
return authorizePasswordChange(context, request);
}
} catch (SQLException e) {
log.error("Something went wrong trying to retrieve EPerson for email: " + email, e);
@@ -656,6 +666,19 @@ public class AuthorizeUtil {
return false;
}
/**
* Checks if the current configuration has at least one password based authentication method
*
* @param context Dspace Context
* @param request Current Request
* @return True if the password change is enabled
* @throws SQLException
*/
protected static boolean authorizePasswordChange(Context context, HttpServletRequest request) throws SQLException {
return AuthenticateServiceFactory.getInstance().getAuthenticationService()
.allowSetPassword(context, request, null);
}
/**
* This method checks if the community Admin can manage accounts
*

View File

@@ -14,7 +14,6 @@ import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
@@ -150,17 +149,16 @@ public class DCInputsReader {
* Returns the set of DC inputs used for a particular collection, or the
* default set if no inputs defined for the collection
*
* @param collectionHandle collection's unique Handle
* @param collection collection for which search the set of DC inputs
* @return DC input set
* @throws DCInputsReaderException if no default set defined
* @throws ServletException
*/
public List<DCInputSet> getInputsByCollectionHandle(String collectionHandle)
public List<DCInputSet> getInputsByCollection(Collection collection)
throws DCInputsReaderException {
SubmissionConfig config;
try {
config = SubmissionServiceFactory.getInstance().getSubmissionConfigService()
.getSubmissionConfigByCollection(collectionHandle);
.getSubmissionConfigByCollection(collection);
String formName = config.getSubmissionName();
if (formName == null) {
throw new DCInputsReaderException("No form designated as default");
@@ -691,7 +689,7 @@ public class DCInputsReader {
public String getInputFormNameByCollectionAndField(Collection collection, String field)
throws DCInputsReaderException {
List<DCInputSet> inputSets = getInputsByCollectionHandle(collection.getHandle());
List<DCInputSet> inputSets = getInputsByCollection(collection);
for (DCInputSet inputSet : inputSets) {
String[] tokenized = Utils.tokenize(field);
String schema = tokenized[0];

View File

@@ -11,6 +11,7 @@ import java.io.File;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -21,6 +22,7 @@ import javax.xml.parsers.FactoryConfigurationError;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.DSpaceObject;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.CollectionService;
@@ -90,6 +92,13 @@ public class SubmissionConfigReader {
*/
private Map<String, String> collectionToSubmissionConfig = null;
/**
* Hashmap which stores which submission process configuration is used by
* which community, computed from the item submission config file
* (specifically, the 'submission-map' tag)
*/
private Map<String, String> communityToSubmissionConfig = null;
/**
* Reference to the global submission step definitions defined in the
* "step-definitions" section
@@ -127,6 +136,7 @@ public class SubmissionConfigReader {
public void reload() throws SubmissionConfigReaderException {
collectionToSubmissionConfig = null;
communityToSubmissionConfig = null;
stepDefns = null;
submitDefns = null;
buildInputs(configDir + SUBMIT_DEF_FILE_PREFIX + SUBMIT_DEF_FILE_SUFFIX);
@@ -145,7 +155,8 @@ public class SubmissionConfigReader {
*/
private void buildInputs(String fileName) throws SubmissionConfigReaderException {
collectionToSubmissionConfig = new HashMap<String, String>();
submitDefns = new HashMap<String, List<Map<String, String>>>();
communityToSubmissionConfig = new HashMap<String, String>();
submitDefns = new LinkedHashMap<String, List<Map<String, String>>>();
String uri = "file:" + new File(fileName).getAbsolutePath();
@@ -210,18 +221,41 @@ public class SubmissionConfigReader {
* Returns the Item Submission process config used for a particular
* collection, or the default if none is defined for the collection
*
* @param collectionHandle collection's unique Handle
* @param col collection for which search Submission process config
* @return the SubmissionConfig representing the item submission config
* @throws SubmissionConfigReaderException if no default submission process configuration defined
* @throws IllegalStateException if no default submission process configuration defined
*/
public SubmissionConfig getSubmissionConfigByCollection(String collectionHandle) {
// get the name of the submission process config for this collection
String submitName = collectionToSubmissionConfig
.get(collectionHandle);
if (submitName == null) {
public SubmissionConfig getSubmissionConfigByCollection(Collection col) {
String submitName;
if (col != null) {
// get the name of the submission process config for this collection
submitName = collectionToSubmissionConfig
.get(DEFAULT_COLLECTION);
.get(col.getHandle());
if (submitName != null) {
return getSubmissionConfigByName(submitName);
}
if (!communityToSubmissionConfig.isEmpty()) {
try {
List<Community> communities = col.getCommunities();
for (Community com : communities) {
submitName = getSubmissionConfigByCommunity(com);
if (submitName != null) {
return getSubmissionConfigByName(submitName);
}
}
} catch (SQLException sqle) {
throw new IllegalStateException("Error occurred while getting item submission configured " +
"by community", sqle);
}
}
}
submitName = collectionToSubmissionConfig.get(DEFAULT_COLLECTION);
if (submitName == null) {
throw new IllegalStateException(
"No item submission process configuration designated as 'default' in 'submission-map' section of " +
@@ -230,6 +264,30 @@ public class SubmissionConfigReader {
return getSubmissionConfigByName(submitName);
}
/**
* Recursive function to return the Item Submission process config
* used for a community or the closest community parent, or null
* if none is defined
*
* @param com community for which search Submission process config
* @return the SubmissionConfig representing the item submission config
*/
private String getSubmissionConfigByCommunity(Community com) {
String submitName = communityToSubmissionConfig
.get(com.getHandle());
if (submitName != null) {
return submitName;
}
List<Community> communities = com.getParentCommunities();
for (Community parentCom : communities) {
submitName = getSubmissionConfigByCommunity(parentCom);
if (submitName != null) {
return submitName;
}
}
return null;
}
/**
* Returns the Item Submission process config
*
@@ -357,13 +415,14 @@ public class SubmissionConfigReader {
Node nd = nl.item(i);
if (nd.getNodeName().equals("name-map")) {
String id = getAttribute(nd, "collection-handle");
String communityId = getAttribute(nd, "community-handle");
String entityType = getAttribute(nd, "collection-entity-type");
String value = getAttribute(nd, "submission-name");
String content = getValue(nd);
if (id == null && entityType == null) {
if (id == null && communityId == null && entityType == null) {
throw new SAXException(
"name-map element is missing collection-handle or collection-entity-type attribute " +
"in 'item-submission.xml'");
"name-map element is missing collection-handle or community-handle or collection-entity-type " +
"attribute in 'item-submission.xml'");
}
if (value == null) {
throw new SAXException(
@@ -375,7 +434,8 @@ public class SubmissionConfigReader {
}
if (id != null) {
collectionToSubmissionConfig.put(id, value);
} else if (communityId != null) {
communityToSubmissionConfig.put(communityId, value);
} else {
// get all collections for this entity-type
List<Collection> collections = collectionService.findAllCollectionsByEntityType( context,

View File

@@ -405,21 +405,13 @@ public class Util {
DCInput myInputs = null;
boolean myInputsFound = false;
String formFileName = I18nUtil.getInputFormsFileName(locale);
String col_handle = "";
Collection collection = item.getOwningCollection();
if (collection == null) {
// set an empty handle so to get the default input set
col_handle = "";
} else {
col_handle = collection.getHandle();
}
// Read the input form file for the specific collection
DCInputsReader inputsReader = new DCInputsReader(formFileName);
List<DCInputSet> inputSets = inputsReader.getInputsByCollectionHandle(col_handle);
List<DCInputSet> inputSets = inputsReader.getInputsByCollection(collection);
// Replace the values of Metadatum[] with the correct ones in case
// of
@@ -500,8 +492,8 @@ public class Util {
public static List<String> differenceInSubmissionFields(Collection fromCollection, Collection toCollection)
throws DCInputsReaderException {
DCInputsReader reader = new DCInputsReader();
List<DCInputSet> from = reader.getInputsByCollectionHandle(fromCollection.getHandle());
List<DCInputSet> to = reader.getInputsByCollectionHandle(toCollection.getHandle());
List<DCInputSet> from = reader.getInputsByCollection(fromCollection);
List<DCInputSet> to = reader.getInputsByCollection(toCollection);
Set<String> fromFieldName = new HashSet<>();
Set<String> toFieldName = new HashSet<>();

View File

@@ -80,7 +80,15 @@ public class DSpaceAuthorityIndexer implements AuthorityIndexerInterface, Initia
throws SQLException, AuthorizeException {
List<AuthorityValue> values = new ArrayList<>();
for (String metadataField : metadataFields) {
List<MetadataValue> metadataValues = itemService.getMetadataByMetadataString(item, metadataField);
String[] fieldParts = metadataField.split("\\.");
String schema = (fieldParts.length > 0 ? fieldParts[0] : null);
String element = (fieldParts.length > 1 ? fieldParts[1] : null);
String qualifier = (fieldParts.length > 2 ? fieldParts[2] : null);
// Get metadata values without virtual metadata
List<MetadataValue> metadataValues = itemService.getMetadata(item, schema, element, qualifier, Item.ANY,
false);
for (MetadataValue metadataValue : metadataValues) {
String content = metadataValue.getValue();
String authorityKey = metadataValue.getAuthority();

View File

@@ -895,7 +895,7 @@ public class AuthorizeServiceImpl implements AuthorizeService {
return true;
}
} catch (SearchServiceException e) {
log.error("Failed getting getting community/collection admin status for "
log.error("Failed getting community/collection admin status for "
+ context.getCurrentUser().getEmail() + " The search error is: " + e.getMessage()
+ " The search resourceType filter was: " + query);
}

View File

@@ -15,19 +15,19 @@ import java.util.Map;
*
* @author Mohamed Eskander (mohamed.eskander at 4science.com)
*/
public class COARNotifyConfigurationService {
public class NotifyConfigurationService {
/**
* Mapping the submission step process identifier with the configuration
* (see configuration at coar-notify.xml)
*/
private Map<String, List<LDNPattern>> patterns;
private Map<String, List<NotifyPattern>> patterns;
public Map<String, List<LDNPattern>> getPatterns() {
public Map<String, List<NotifyPattern>> getPatterns() {
return patterns;
}
public void setPatterns(Map<String, List<LDNPattern>> patterns) {
public void setPatterns(Map<String, List<NotifyPattern>> patterns) {
this.patterns = patterns;
}

View File

@@ -12,16 +12,16 @@ package org.dspace.coarnotify;
*
* @author Mohamed Eskander (mohamed.eskander at 4science.com)
*/
public class LDNPattern {
public class NotifyPattern {
private String pattern;
private boolean multipleRequest;
public LDNPattern() {
public NotifyPattern() {
}
public LDNPattern(String pattern, boolean multipleRequest) {
public NotifyPattern(String pattern, boolean multipleRequest) {
this.pattern = pattern;
this.multipleRequest = multipleRequest;
}

View File

@@ -14,7 +14,7 @@ import java.util.List;
*
* @author Mohamed Eskander (mohamed.eskander at 4science.com)
*/
public class COARNotifySubmissionConfiguration {
public class NotifySubmissionConfiguration {
/**
* the map key of configured bean of COARNotifyConfigurationService
@@ -26,13 +26,13 @@ public class COARNotifySubmissionConfiguration {
* the map values of configured bean of COARNotifyConfigurationService
* in coar-notify.xml
*/
private List<LDNPattern> patterns;
private List<NotifyPattern> patterns;
public COARNotifySubmissionConfiguration() {
public NotifySubmissionConfiguration() {
}
public COARNotifySubmissionConfiguration(String id, List<LDNPattern> patterns) {
public NotifySubmissionConfiguration(String id, List<NotifyPattern> patterns) {
super();
this.id = id;
this.patterns = patterns;
@@ -51,7 +51,7 @@ public class COARNotifySubmissionConfiguration {
*
* @return the list of configured COAR Notify Patterns
*/
public List<LDNPattern> getPatterns() {
public List<NotifyPattern> getPatterns() {
return patterns;
}
@@ -59,7 +59,7 @@ public class COARNotifySubmissionConfiguration {
* Sets the list of configured COAR Notify Patterns
* @param patterns
*/
public void setPatterns(final List<LDNPattern> patterns) {
public void setPatterns(final List<NotifyPattern> patterns) {
this.patterns = patterns;
}
}

View File

@@ -10,41 +10,41 @@ package org.dspace.coarnotify;
import java.util.ArrayList;
import java.util.List;
import org.dspace.coarnotify.service.SubmissionCOARNotifyService;
import org.dspace.coarnotify.service.SubmissionNotifyService;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Service implementation of {@link SubmissionCOARNotifyService}
* Service implementation of {@link SubmissionNotifyService}
*
* @author Mohamed Eskander (mohamed.eskander at 4science.com)
*/
public class SubmissionCOARNotifyServiceImpl implements SubmissionCOARNotifyService {
public class SubmissionNotifyServiceImpl implements SubmissionNotifyService {
@Autowired(required = true)
private COARNotifyConfigurationService coarNotifyConfigurationService;
private NotifyConfigurationService coarNotifyConfigurationService;
protected SubmissionCOARNotifyServiceImpl() {
protected SubmissionNotifyServiceImpl() {
}
@Override
public COARNotifySubmissionConfiguration findOne(String id) {
List<LDNPattern> patterns =
public NotifySubmissionConfiguration findOne(String id) {
List<NotifyPattern> patterns =
coarNotifyConfigurationService.getPatterns().get(id);
if (patterns == null) {
return null;
}
return new COARNotifySubmissionConfiguration(id, patterns);
return new NotifySubmissionConfiguration(id, patterns);
}
@Override
public List<COARNotifySubmissionConfiguration> findAll() {
List<COARNotifySubmissionConfiguration> coarNotifies = new ArrayList<>();
public List<NotifySubmissionConfiguration> findAll() {
List<NotifySubmissionConfiguration> coarNotifies = new ArrayList<>();
coarNotifyConfigurationService.getPatterns().forEach((id, patterns) ->
coarNotifies.add(new COARNotifySubmissionConfiguration(id, patterns)
coarNotifies.add(new NotifySubmissionConfiguration(id, patterns)
));
return coarNotifies;

View File

@@ -9,7 +9,7 @@ package org.dspace.coarnotify.service;
import java.util.List;
import org.dspace.coarnotify.COARNotifySubmissionConfiguration;
import org.dspace.coarnotify.NotifySubmissionConfiguration;
/**
* Service interface class for the Creative Submission COAR Notify.
@@ -18,7 +18,7 @@ import org.dspace.coarnotify.COARNotifySubmissionConfiguration;
*
* @author Mohamed Eskander (mohamed.eskander at 4science.com)
*/
public interface SubmissionCOARNotifyService {
public interface SubmissionNotifyService {
/**
* Find the COARE Notify corresponding to the provided ID
@@ -27,13 +27,13 @@ public interface SubmissionCOARNotifyService {
* @param id - the ID of the COAR Notify to be found
* @return the corresponding COAR Notify if found or null when not found
*/
public COARNotifySubmissionConfiguration findOne(String id);
public NotifySubmissionConfiguration findOne(String id);
/**
* Find all configured COAR Notifies
*
* @return all configured COAR Notifies
*/
public List<COARNotifySubmissionConfiguration> findAll();
public List<NotifySubmissionConfiguration> findAll();
}

View File

@@ -307,10 +307,18 @@ public class Bitstream extends DSpaceObject implements DSpaceObjectLegacySupport
return collection;
}
public void setCollection(Collection collection) {
this.collection = collection;
}
public Community getCommunity() {
return community;
}
public void setCommunity(Community community) {
this.community = community;
}
/**
* Get the asset store number where this bitstream is stored
*

View File

@@ -458,10 +458,15 @@ public class BitstreamServiceImpl extends DSpaceObjectServiceImpl<Bitstream> imp
@Override
public Bitstream findByIdOrLegacyId(Context context, String id) throws SQLException {
if (StringUtils.isNumeric(id)) {
return findByLegacyId(context, Integer.parseInt(id));
} else {
return find(context, UUID.fromString(id));
try {
if (StringUtils.isNumeric(id)) {
return findByLegacyId(context, Integer.parseInt(id));
} else {
return find(context, UUID.fromString(id));
}
} catch (IllegalArgumentException e) {
// Not a valid legacy ID or valid UUID
return null;
}
}

View File

@@ -562,10 +562,15 @@ public class BundleServiceImpl extends DSpaceObjectServiceImpl<Bundle> implement
@Override
public Bundle findByIdOrLegacyId(Context context, String id) throws SQLException {
if (StringUtils.isNumeric(id)) {
return findByLegacyId(context, Integer.parseInt(id));
} else {
return find(context, UUID.fromString(id));
try {
if (StringUtils.isNumeric(id)) {
return findByLegacyId(context, Integer.parseInt(id));
} else {
return find(context, UUID.fromString(id));
}
} catch (IllegalArgumentException e) {
// Not a valid legacy ID or valid UUID
return null;
}
}

View File

@@ -135,6 +135,9 @@ public class Collection extends DSpaceObject implements DSpaceObjectLegacySuppor
protected void setLogo(Bitstream logo) {
this.logo = logo;
if (logo != null) {
logo.setCollection(this);
}
setModified();
}

View File

@@ -895,10 +895,15 @@ public class CollectionServiceImpl extends DSpaceObjectServiceImpl<Collection> i
@Override
public Collection findByIdOrLegacyId(Context context, String id) throws SQLException {
if (StringUtils.isNumeric(id)) {
return findByLegacyId(context, Integer.parseInt(id));
} else {
return find(context, UUID.fromString(id));
try {
if (StringUtils.isNumeric(id)) {
return findByLegacyId(context, Integer.parseInt(id));
} else {
return find(context, UUID.fromString(id));
}
} catch (IllegalArgumentException e) {
// Not a valid legacy ID or valid UUID
return null;
}
}

View File

@@ -123,6 +123,9 @@ public class Community extends DSpaceObject implements DSpaceObjectLegacySupport
void setLogo(Bitstream logo) {
this.logo = logo;
if (logo != null) {
logo.setCommunity(this);
}
setModified();
}

View File

@@ -694,10 +694,15 @@ public class CommunityServiceImpl extends DSpaceObjectServiceImpl<Community> imp
@Override
public Community findByIdOrLegacyId(Context context, String id) throws SQLException {
if (StringUtils.isNumeric(id)) {
return findByLegacyId(context, Integer.parseInt(id));
} else {
return find(context, UUID.fromString(id));
try {
if (StringUtils.isNumeric(id)) {
return findByLegacyId(context, Integer.parseInt(id));
} else {
return find(context, UUID.fromString(id));
}
} catch (IllegalArgumentException e) {
// Not a valid legacy ID or valid UUID
return null;
}
}

View File

@@ -150,7 +150,6 @@ public class InstallItemServiceImpl implements InstallItemService {
return finishItem(c, item, is);
}
protected void populateMetadata(Context c, Item item)
throws SQLException, AuthorizeException {
// create accession date
@@ -158,15 +157,6 @@ public class InstallItemServiceImpl implements InstallItemService {
itemService.addMetadata(c, item, MetadataSchemaEnum.DC.getName(),
"date", "accessioned", null, now.toString());
// add date available if not under embargo, otherwise it will
// be set when the embargo is lifted.
// this will flush out fatal embargo metadata
// problems before we set inArchive.
if (embargoService.getEmbargoTermsAsDate(c, item) == null) {
itemService.addMetadata(c, item, MetadataSchemaEnum.DC.getName(),
"date", "available", null, now.toString());
}
// If issue date is set as "today" (literal string), then set it to current date
// In the below loop, we temporarily clear all issued dates and re-add, one-by-one,
// replacing "today" with today's date.

View File

@@ -8,6 +8,7 @@
package org.dspace.content;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.dspace.app.ldn.ItemFilter;
@@ -37,12 +38,21 @@ public class ItemFilterServiceImpl implements ItemFilterService {
@Override
public List<ItemFilter> findAll() {
return serviceManager.getServicesWithNamesByType(LogicalStatement.class)
.keySet()
.stream()
.sorted()
.map(ItemFilter::new)
.collect(Collectors.toList());
Map<String, LogicalStatement> ldnFilters =
serviceManager.getServiceByName("ldnItemFilters", Map.class);
return ldnFilters.keySet()
.stream()
.sorted()
.map(ItemFilter::new)
.collect(Collectors.toList());
}
public ServiceManager getServiceManager() {
return serviceManager;
}
public void setServiceManager(ServiceManager serviceManager) {
this.serviceManager = serviceManager;
}
}

View File

@@ -49,6 +49,7 @@ import org.dspace.content.service.MetadataSchemaService;
import org.dspace.content.service.RelationshipService;
import org.dspace.content.service.WorkspaceItemService;
import org.dspace.content.virtual.VirtualMetadataPopulator;
import org.dspace.contentreport.QueryPredicate;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.core.LogHelper;
@@ -175,7 +176,6 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl<Item> implements It
private QAEventsDAO qaEventsDao;
protected ItemServiceImpl() {
super();
}
@Override
@@ -275,9 +275,8 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl<Item> implements It
+ template.getID()));
return template;
} else {
return collection.getTemplateItem();
}
return collection.getTemplateItem();
}
@Override
@@ -1190,9 +1189,8 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl<Item> implements It
if (item.getOwningCollection() == null) {
if (!isInProgressSubmission(context, item)) {
return true;
} else {
return false;
}
return false;
}
return collectionService.canEditBoolean(context, item.getOwningCollection(), false);
@@ -1284,8 +1282,8 @@ prevent the generation of resource policy entry values with null dspace_object a
if (!authorizeService
.isAnIdenticalPolicyAlreadyInPlace(context, dso, defaultPolicy.getGroup(), Constants.READ,
defaultPolicy.getID()) &&
(!appendMode && this.isNotAlreadyACustomRPOfThisTypeOnDSO(context, dso) ||
appendMode && this.shouldBeAppended(context, dso, defaultPolicy))) {
(!appendMode && isNotAlreadyACustomRPOfThisTypeOnDSO(context, dso) ||
appendMode && shouldBeAppended(context, dso, defaultPolicy))) {
ResourcePolicy newPolicy = resourcePolicyService.clone(context, defaultPolicy);
newPolicy.setdSpaceObject(dso);
newPolicy.setAction(Constants.READ);
@@ -1384,9 +1382,8 @@ prevent the generation of resource policy entry values with null dspace_object a
if (Item.ANY.equals(value)) {
return itemDAO.findByMetadataField(context, mdf, null, true);
} else {
return itemDAO.findByMetadataField(context, mdf, value, true);
}
return itemDAO.findByMetadataField(context, mdf, value, true);
}
@Override
@@ -1430,19 +1427,24 @@ prevent the generation of resource policy entry values with null dspace_object a
if (Item.ANY.equals(value)) {
return itemDAO.findByMetadataField(context, mdf, null, true);
} else {
return itemDAO.findByMetadataField(context, mdf, value, true);
}
return itemDAO.findByMetadataField(context, mdf, value, true);
}
@Override
public Iterator<Item> findByMetadataQuery(Context context, List<List<MetadataField>> listFieldList,
List<String> query_op, List<String> query_val, List<UUID> collectionUuids,
String regexClause, int offset, int limit)
throws SQLException, AuthorizeException, IOException {
return itemDAO
.findByMetadataQuery(context, listFieldList, query_op, query_val, collectionUuids, regexClause, offset,
limit);
public List<Item> findByMetadataQuery(Context context, List<QueryPredicate> queryPredicates,
List<UUID> collectionUuids, long offset, int limit)
throws SQLException {
return itemDAO.findByMetadataQuery(context, queryPredicates, collectionUuids, "text_value ~ ?",
offset, limit);
}
@Override
public long countForMetadataQuery(Context context, List<QueryPredicate> queryPredicates,
List<UUID> collectionUuids)
throws SQLException {
return itemDAO.countForMetadataQuery(context, queryPredicates, collectionUuids, "text_value ~ ?");
}
@Override
@@ -1508,20 +1510,19 @@ prevent the generation of resource policy entry values with null dspace_object a
Collection ownCollection = item.getOwningCollection();
if (ownCollection != null) {
return ownCollection;
} else {
InProgressSubmission inprogress = ContentServiceFactory.getInstance().getWorkspaceItemService()
.findByItem(context,
item);
if (inprogress == null) {
inprogress = WorkflowServiceFactory.getInstance().getWorkflowItemService().findByItem(context, item);
}
if (inprogress != null) {
return inprogress.getCollection();
}
// is a template item?
return item.getTemplateItemOf();
}
InProgressSubmission inprogress = ContentServiceFactory.getInstance().getWorkspaceItemService()
.findByItem(context,
item);
if (inprogress == null) {
inprogress = WorkflowServiceFactory.getInstance().getWorkflowItemService().findByItem(context, item);
}
if (inprogress != null) {
return inprogress.getCollection();
}
// is a template item?
return item.getTemplateItemOf();
}
@Override
@@ -1618,10 +1619,14 @@ prevent the generation of resource policy entry values with null dspace_object a
@Override
public Item findByIdOrLegacyId(Context context, String id) throws SQLException {
if (StringUtils.isNumeric(id)) {
return findByLegacyId(context, Integer.parseInt(id));
} else {
try {
if (StringUtils.isNumeric(id)) {
return findByLegacyId(context, Integer.parseInt(id));
}
return find(context, UUID.fromString(id));
} catch (IllegalArgumentException e) {
// Not a valid legacy ID or valid UUID
return null;
}
}

View File

@@ -13,6 +13,7 @@ import java.security.NoSuchAlgorithmException;
import java.util.Date;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.dspace.qaevent.service.dto.CorrectionTypeMessageDTO;
import org.dspace.qaevent.service.dto.NotifyMessageDTO;
import org.dspace.qaevent.service.dto.OpenaireMessageDTO;
import org.dspace.qaevent.service.dto.QAMessageDTO;
@@ -32,6 +33,7 @@ public class QAEvent {
public static final String DISCARDED = "discarded";
public static final String OPENAIRE_SOURCE = "openaire";
public static final String DSPACE_USERS_SOURCE = "DSpaceUsers";
public static final String COAR_NOTIFY_SOURCE = "coar-notify";
private String source;
@@ -65,8 +67,7 @@ public class QAEvent {
private String status = "PENDING";
public QAEvent() {
}
public QAEvent() {}
public QAEvent(String source, String originalId, String target, String title,
String topic, double trust, String message, Date lastUpdate) {
@@ -84,7 +85,6 @@ public class QAEvent {
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}
public String getOriginalId() {
@@ -205,18 +205,16 @@ public class QAEvent {
}
public Class<? extends QAMessageDTO> getMessageDtoClass() {
Class<? extends QAMessageDTO> result = null;
switch (getSource()) {
case OPENAIRE_SOURCE:
result = OpenaireMessageDTO.class;
break;
return OpenaireMessageDTO.class;
case COAR_NOTIFY_SOURCE:
result = NotifyMessageDTO.class;
break;
return NotifyMessageDTO.class;
case DSPACE_USERS_SOURCE:
return CorrectionTypeMessageDTO.class;
default:
throw new IllegalArgumentException("Unknown event's source: " + getSource());
}
return result;
}
}

View File

@@ -242,7 +242,7 @@ public final class ChoiceAuthorityServiceImpl implements ChoiceAuthorityService
// check if it is the requested collection
Map<String, ChoiceAuthority> controllerFormDef = controllerFormDefinitions.get(fieldKey);
SubmissionConfig submissionConfig = submissionConfigService
.getSubmissionConfigByCollection(collection.getHandle());
.getSubmissionConfigByCollection(collection);
String submissionName = submissionConfig.getSubmissionName();
// check if the requested collection has a submission definition that use an authority for the metadata
if (controllerFormDef.containsKey(submissionName)) {
@@ -495,7 +495,7 @@ public final class ChoiceAuthorityServiceImpl implements ChoiceAuthorityService
try {
configReaderService = SubmissionServiceFactory.getInstance().getSubmissionConfigService();
SubmissionConfig submissionName = configReaderService
.getSubmissionConfigByCollection(collection.getHandle());
.getSubmissionConfigByCollection(collection);
ma = controllerFormDefinitions.get(fieldKey).get(submissionName.getSubmissionName());
} catch (SubmissionConfigReaderException e) {
// the system is in an illegal state as the submission definition is not valid

View File

@@ -156,7 +156,8 @@ public class DCInputAuthority extends SelfNamedPlugin implements ChoiceAuthority
int found = 0;
List<Choice> v = new ArrayList<Choice>();
for (int i = 0; i < valuesLocale.length; ++i) {
if (query == null || StringUtils.containsIgnoreCase(valuesLocale[i], query)) {
// In a DCInputAuthority context, a user will want to query the labels, not the values
if (query == null || StringUtils.containsIgnoreCase(labelsLocale[i], query)) {
if (found >= start && v.size() < limit) {
v.add(new Choice(null, valuesLocale[i], labelsLocale[i]));
if (valuesLocale[i].equalsIgnoreCase(query)) {

View File

@@ -59,7 +59,37 @@ public class SHERPARoMEOJournalTitle implements ChoiceAuthority {
@Override
public Choices getBestMatch(String text, String locale) {
return getMatches(text, 0, 1, locale);
// punt if there is no query text
if (text == null || text.trim().length() == 0) {
return new Choices(true);
}
int limit = 10;
SHERPAService sherpaService = new DSpace().getSingletonService(SHERPAService.class);
SHERPAResponse sherpaResponse = sherpaService.performRequest("publication", "title",
"equals", text, 0, limit);
Choices result;
if (CollectionUtils.isNotEmpty(sherpaResponse.getJournals())) {
List<Choice> list = sherpaResponse
.getJournals().stream()
.map(sherpaJournal -> new Choice(sherpaJournal.getIssns().get(0),
sherpaJournal.getTitles().get(0), sherpaJournal.getTitles().get(0)))
.collect(Collectors.toList());
int total = sherpaResponse.getJournals().size();
int confidence;
if (list.isEmpty()) {
confidence = Choices.CF_NOTFOUND;
} else if (list.size() == 1) {
confidence = Choices.CF_UNCERTAIN;
} else {
confidence = Choices.CF_AMBIGUOUS;
}
result = new Choices(list.toArray(new Choice[list.size()]), 0, total, confidence,
total > limit);
} else {
result = new Choices(false);
}
return result;
}
@Override

View File

@@ -60,7 +60,38 @@ public class SHERPARoMEOPublisher implements ChoiceAuthority {
@Override
public Choices getBestMatch(String text, String locale) {
return getMatches(text, 0, 1, locale);
// punt if there is no query text
if (text == null || text.trim().length() == 0) {
return new Choices(true);
}
int limit = 10;
SHERPAService sherpaService = new DSpace().getSingletonService(SHERPAService.class);
SHERPAPublisherResponse sherpaResponse = sherpaService.performPublisherRequest("publisher", "name",
"equals", text, 0, limit);
Choices result;
if (CollectionUtils.isNotEmpty(sherpaResponse.getPublishers())) {
List<Choice> list = sherpaResponse
.getPublishers().stream()
.map(sherpaPublisher ->
new Choice(sherpaPublisher.getIdentifier(),
sherpaPublisher.getName(), sherpaPublisher.getName()))
.collect(Collectors.toList());
int total = sherpaResponse.getPublishers().size();
int confidence;
if (list.isEmpty()) {
confidence = Choices.CF_NOTFOUND;
} else if (list.size() == 1) {
confidence = Choices.CF_UNCERTAIN;
} else {
confidence = Choices.CF_AMBIGUOUS;
}
result = new Choices(list.toArray(new Choice[list.size()]), 0, total, confidence,
total > limit);
} else {
result = new Choices(false);
}
return result;
}
@Override

View File

@@ -17,6 +17,7 @@ import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.Item;
import org.dspace.content.MetadataField;
import org.dspace.contentreport.QueryPredicate;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
@@ -28,12 +29,11 @@ import org.dspace.eperson.EPerson;
* @author kevinvandevelde at atmire.com
*/
public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> {
public Iterator<Item> findAll(Context context, boolean archived) throws SQLException;
Iterator<Item> findAll(Context context, boolean archived) throws SQLException;
public Iterator<Item> findAll(Context context, boolean archived, int limit, int offset) throws SQLException;
Iterator<Item> findAll(Context context, boolean archived, int limit, int offset) throws SQLException;
@Deprecated
public Iterator<Item> findAll(Context context, boolean archived, boolean withdrawn) throws SQLException;
@Deprecated Iterator<Item> findAll(Context context, boolean archived, boolean withdrawn) throws SQLException;
/**
* Find all items that are:
@@ -46,7 +46,7 @@ public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> {
* @return iterator over all regular items.
* @throws SQLException if database error.
*/
public Iterator<Item> findAllRegularItems(Context context) throws SQLException;
Iterator<Item> findAllRegularItems(Context context) throws SQLException;
/**
* Find all Items modified since a Date.
@@ -56,10 +56,10 @@ public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> {
* @return iterator over items
* @throws SQLException if database error
*/
public Iterator<Item> findByLastModifiedSince(Context context, Date since)
Iterator<Item> findByLastModifiedSince(Context context, Date since)
throws SQLException;
public Iterator<Item> findBySubmitter(Context context, EPerson eperson) throws SQLException;
Iterator<Item> findBySubmitter(Context context, EPerson eperson) throws SQLException;
/**
* Find all the items by a given submitter. The order is
@@ -71,23 +71,40 @@ public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> {
* @return an iterator over the items submitted by eperson
* @throws SQLException if database error
*/
public Iterator<Item> findBySubmitter(Context context, EPerson eperson, boolean retrieveAllItems)
Iterator<Item> findBySubmitter(Context context, EPerson eperson, boolean retrieveAllItems)
throws SQLException;
public Iterator<Item> findBySubmitter(Context context, EPerson eperson, MetadataField metadataField, int limit)
Iterator<Item> findBySubmitter(Context context, EPerson eperson, MetadataField metadataField, int limit)
throws SQLException;
public Iterator<Item> findByMetadataField(Context context, MetadataField metadataField, String value,
Iterator<Item> findByMetadataField(Context context, MetadataField metadataField, String value,
boolean inArchive) throws SQLException;
public Iterator<Item> findByMetadataQuery(Context context, List<List<MetadataField>> listFieldList,
List<String> query_op, List<String> query_val, List<UUID> collectionUuids,
String regexClause, int offset, int limit) throws SQLException;
/**
* Returns all the Items that belong to the specified aollections (if any)
* and match the provided predicates.
* @param context The relevant DSpace context
* @param queryPredicates List of predicates that returned items are required to match
* @param collectionUuids UUIDs of the collections to search.
* If none are provided, the entire repository will be searched.
* @param regexClause Syntactic expression used to query the database using a regular expression
* (e.g.: "text_value ~ ?")
* @param offset The offset for the query
* @param limit Maximum number of items to return
* @return A list containing the items that match the provided criteria
* @throws SQLException if something goes wrong
*/
List<Item> findByMetadataQuery(Context context, List<QueryPredicate> queryPredicates,
List<UUID> collectionUuids, String regexClause,
long offset, int limit) throws SQLException;
public Iterator<Item> findByAuthorityValue(Context context, MetadataField metadataField, String authority,
long countForMetadataQuery(Context context, List<QueryPredicate> queryPredicates,
List<UUID> collectionUuids, String regexClause) throws SQLException;
Iterator<Item> findByAuthorityValue(Context context, MetadataField metadataField, String authority,
boolean inArchive) throws SQLException;
public Iterator<Item> findArchivedByCollection(Context context, Collection collection, Integer limit,
Iterator<Item> findArchivedByCollection(Context context, Collection collection, Integer limit,
Integer offset) throws SQLException;
/**
@@ -100,7 +117,7 @@ public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> {
* @return An iterator containing the items for which the constraints hold true
* @throws SQLException If something goes wrong
*/
public Iterator<Item> findArchivedByCollectionExcludingOwning(Context context, Collection collection, Integer limit,
Iterator<Item> findArchivedByCollectionExcludingOwning(Context context, Collection collection, Integer limit,
Integer offset) throws SQLException;
/**
@@ -111,11 +128,11 @@ public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> {
* @return The total amount of items that fit the constraints
* @throws SQLException If something goes wrong
*/
public int countArchivedByCollectionExcludingOwning(Context context, Collection collection) throws SQLException;
int countArchivedByCollectionExcludingOwning(Context context, Collection collection) throws SQLException;
public Iterator<Item> findAllByCollection(Context context, Collection collection) throws SQLException;
Iterator<Item> findAllByCollection(Context context, Collection collection) throws SQLException;
public Iterator<Item> findAllByCollection(Context context, Collection collection, Integer limit, Integer offset)
Iterator<Item> findAllByCollection(Context context, Collection collection, Integer limit, Integer offset)
throws SQLException;
/**
@@ -128,7 +145,7 @@ public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> {
* @return item count
* @throws SQLException if database error
*/
public int countItems(Context context, Collection collection, boolean includeArchived, boolean includeWithdrawn)
int countItems(Context context, Collection collection, boolean includeArchived, boolean includeWithdrawn)
throws SQLException;
/**
@@ -144,7 +161,7 @@ public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> {
* @return item count
* @throws SQLException if database error
*/
public int countItems(Context context, List<Collection> collections, boolean includeArchived,
int countItems(Context context, List<Collection> collections, boolean includeArchived,
boolean includeWithdrawn) throws SQLException;
/**
@@ -158,7 +175,7 @@ public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> {
* @return iterator over items
* @throws SQLException if database error
*/
public Iterator<Item> findAll(Context context, boolean archived,
Iterator<Item> findAll(Context context, boolean archived,
boolean withdrawn, boolean discoverable, Date lastModified)
throws SQLException;
@@ -192,7 +209,7 @@ public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> {
* @return count of items
* @throws SQLException if database error
*/
public int countItems(Context context, EPerson submitter, boolean includeArchived, boolean includeWithdrawn)
int countItems(Context context, EPerson submitter, boolean includeArchived, boolean includeWithdrawn)
throws SQLException;
}

View File

@@ -8,6 +8,7 @@
package org.dspace.content.dao.impl;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
@@ -16,28 +17,27 @@ import java.util.UUID;
import javax.persistence.Query;
import javax.persistence.TemporalType;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaBuilder.In;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Subquery;
import org.apache.logging.log4j.Logger;
import org.dspace.content.Collection;
import org.dspace.content.DSpaceObject_;
import org.dspace.content.Item;
import org.dspace.content.Item_;
import org.dspace.content.MetadataField;
import org.dspace.content.MetadataValue;
import org.dspace.content.MetadataValue_;
import org.dspace.content.dao.ItemDAO;
import org.dspace.contentreport.QueryOperator;
import org.dspace.contentreport.QueryPredicate;
import org.dspace.core.AbstractHibernateDSODAO;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.hibernate.Criteria;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Property;
import org.hibernate.criterion.Restrictions;
import org.hibernate.criterion.Subqueries;
import org.hibernate.type.StandardBasicTypes;
import org.dspace.util.JpaCriteriaBuilderKit;
/**
* Hibernate implementation of the Database Access Object interface class for the Item object.
@@ -50,7 +50,6 @@ public class ItemDAOImpl extends AbstractHibernateDSODAO<Item> implements ItemDA
private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(ItemDAOImpl.class);
protected ItemDAOImpl() {
super();
}
@Override
@@ -174,118 +173,103 @@ public class ItemDAOImpl extends AbstractHibernateDSODAO<Item> implements ItemDA
return iterate(query);
}
enum OP {
equals {
public Criterion buildPredicate(String val, String regexClause) {
return Property.forName("mv.value").eq(val);
}
},
not_equals {
public Criterion buildPredicate(String val, String regexClause) {
return OP.equals.buildPredicate(val, regexClause);
}
},
like {
public Criterion buildPredicate(String val, String regexClause) {
return Property.forName("mv.value").like(val);
}
},
not_like {
public Criterion buildPredicate(String val, String regexClause) {
return OP.like.buildPredicate(val, regexClause);
}
},
contains {
public Criterion buildPredicate(String val, String regexClause) {
return Property.forName("mv.value").like("%" + val + "%");
}
},
doesnt_contain {
public Criterion buildPredicate(String val, String regexClause) {
return OP.contains.buildPredicate(val, regexClause);
}
},
exists {
public Criterion buildPredicate(String val, String regexClause) {
return Property.forName("mv.value").isNotNull();
}
},
doesnt_exist {
public Criterion buildPredicate(String val, String regexClause) {
return OP.exists.buildPredicate(val, regexClause);
}
},
matches {
public Criterion buildPredicate(String val, String regexClause) {
return Restrictions.sqlRestriction(regexClause, val, StandardBasicTypes.STRING);
}
},
doesnt_match {
public Criterion buildPredicate(String val, String regexClause) {
return OP.matches.buildPredicate(val, regexClause);
}
};
public abstract Criterion buildPredicate(String val, String regexClause);
@Override
public List<Item> findByMetadataQuery(Context context, List<QueryPredicate> queryPredicates,
List<UUID> collectionUuids, String regexClause, long offset, int limit) throws SQLException {
CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
CriteriaQuery<Item> criteriaQuery = getCriteriaQuery(criteriaBuilder, Item.class);
Root<Item> itemRoot = criteriaQuery.from(Item.class);
criteriaQuery.select(itemRoot);
List<Predicate> predicates = toPredicates(criteriaBuilder, criteriaQuery, itemRoot,
queryPredicates, collectionUuids, regexClause);
criteriaQuery.where(criteriaBuilder.and(predicates.stream().toArray(Predicate[]::new)));
criteriaQuery.orderBy(criteriaBuilder.asc(itemRoot.get(DSpaceObject_.id)));
criteriaQuery.groupBy(itemRoot.get(DSpaceObject_.id));
try {
return list(context, criteriaQuery, false, Item.class, limit, (int) offset);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
@Override
@Deprecated
public Iterator<Item> findByMetadataQuery(Context context, List<List<MetadataField>> listFieldList,
List<String> query_op, List<String> query_val, List<UUID> collectionUuids,
String regexClause, int offset, int limit) throws SQLException {
public long countForMetadataQuery(Context context, List<QueryPredicate> queryPredicates,
List<UUID> collectionUuids, String regexClause) throws SQLException {
// Build the query infrastructure
CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
CriteriaQuery<Item> criteriaQuery = getCriteriaQuery(criteriaBuilder, Item.class);
// Select
Root<Item> itemRoot = criteriaQuery.from(Item.class);
// Apply the selected predicates
List<Predicate> predicates = toPredicates(criteriaBuilder, criteriaQuery, itemRoot,
queryPredicates, collectionUuids, regexClause);
criteriaQuery.where(criteriaBuilder.and(predicates.stream().toArray(Predicate[]::new)));
// Execute the query
return countLong(context, criteriaQuery, criteriaBuilder, itemRoot);
}
Criteria criteria = getHibernateSession(context).createCriteria(Item.class, "item");
criteria.setFirstResult(offset);
criteria.setMaxResults(limit);
private <T> List<Predicate> toPredicates(CriteriaBuilder criteriaBuilder, CriteriaQuery<T> query,
Root<Item> root, List<QueryPredicate> queryPredicates,
List<UUID> collectionUuids, String regexClause) {
List<Predicate> predicates = new ArrayList<>();
if (!collectionUuids.isEmpty()) {
DetachedCriteria dcollCriteria = DetachedCriteria.forClass(Collection.class, "coll");
dcollCriteria.setProjection(Projections.property("coll.id"));
dcollCriteria.add(Restrictions.eqProperty("coll.id", "item.owningCollection"));
dcollCriteria.add(Restrictions.in("coll.id", collectionUuids));
criteria.add(Subqueries.exists(dcollCriteria));
Subquery<Collection> scollQuery = query.subquery(Collection.class);
Root<Collection> collRoot = scollQuery.from(Collection.class);
In<UUID> inColls = criteriaBuilder.in(collRoot.get(DSpaceObject_.ID));
collectionUuids.forEach(inColls::value);
scollQuery.select(collRoot.get(DSpaceObject_.ID))
.where(criteriaBuilder.and(
criteriaBuilder.equal(collRoot.get(DSpaceObject_.ID),
root.get(Item_.OWNING_COLLECTION).get(DSpaceObject_.ID)),
collRoot.get(DSpaceObject_.ID).in(collectionUuids)
));
predicates.add(criteriaBuilder.exists(scollQuery));
}
int index = Math.min(listFieldList.size(), Math.min(query_op.size(), query_val.size()));
StringBuilder sb = new StringBuilder();
for (int i = 0; i < index; i++) {
OP op = OP.valueOf(query_op.get(i));
for (int i = 0; i < queryPredicates.size(); i++) {
QueryPredicate predicate = queryPredicates.get(i);
QueryOperator op = predicate.getOperator();
if (op == null) {
log.warn("Skipping Invalid Operator: " + query_op.get(i));
log.warn("Skipping Invalid Operator: null");
continue;
}
if (op == OP.matches || op == OP.doesnt_match) {
if (op.getUsesRegex()) {
if (regexClause.isEmpty()) {
log.warn("Skipping Unsupported Regex Operator: " + query_op.get(i));
log.warn("Skipping Unsupported Regex Operator: " + op);
continue;
}
}
DetachedCriteria subcriteria = DetachedCriteria.forClass(MetadataValue.class, "mv");
subcriteria.add(Property.forName("mv.dSpaceObject").eqProperty("item.id"));
subcriteria.setProjection(Projections.property("mv.dSpaceObject"));
List<Predicate> mvPredicates = new ArrayList<>();
Subquery<MetadataValue> mvQuery = query.subquery(MetadataValue.class);
Root<MetadataValue> mvRoot = mvQuery.from(MetadataValue.class);
mvPredicates.add(criteriaBuilder.equal(
mvRoot.get(MetadataValue_.D_SPACE_OBJECT), root.get(DSpaceObject_.ID)));
if (!listFieldList.get(i).isEmpty()) {
subcriteria.add(Restrictions.in("metadataField", listFieldList.get(i)));
if (!predicate.getFields().isEmpty()) {
In<MetadataField> inFields = criteriaBuilder.in(mvRoot.get(MetadataValue_.METADATA_FIELD));
predicate.getFields().forEach(inFields::value);
mvPredicates.add(inFields);
}
subcriteria.add(op.buildPredicate(query_val.get(i), regexClause));
JpaCriteriaBuilderKit<MetadataValue> jpaKit = new JpaCriteriaBuilderKit<>(criteriaBuilder, mvQuery, mvRoot);
mvPredicates.add(op.buildJpaPredicate(predicate.getValue(), regexClause, jpaKit));
if (op == OP.exists || op == OP.equals || op == OP.like || op == OP.contains || op == OP.matches) {
criteria.add(Subqueries.exists(subcriteria));
mvQuery.select(mvRoot.get(MetadataValue_.D_SPACE_OBJECT))
.where(mvPredicates.stream().toArray(Predicate[]::new));
if (op.getNegate()) {
predicates.add(criteriaBuilder.not(criteriaBuilder.exists(mvQuery)));
} else {
criteria.add(Subqueries.notExists(subcriteria));
predicates.add(criteriaBuilder.exists(mvQuery));
}
}
criteria.addOrder(Order.asc("item.id"));
log.debug(String.format("Running custom query with %d filters", index));
return ((List<Item>) criteria.list()).iterator();
log.debug(String.format("Running custom query with %d filters", queryPredicates.size()));
return predicates;
}
@Override
@@ -322,22 +306,22 @@ public class ItemDAOImpl extends AbstractHibernateDSODAO<Item> implements ItemDA
public Iterator<Item> findArchivedByCollectionExcludingOwning(Context context, Collection collection, Integer limit,
Integer offset) throws SQLException {
CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Item.class);
CriteriaQuery<Item> criteriaQuery = getCriteriaQuery(criteriaBuilder, Item.class);
Root<Item> itemRoot = criteriaQuery.from(Item.class);
criteriaQuery.select(itemRoot);
criteriaQuery.where(criteriaBuilder.and(
criteriaBuilder.notEqual(itemRoot.get(Item_.owningCollection), collection),
criteriaBuilder.isMember(collection, itemRoot.get(Item_.collections)),
criteriaBuilder.isTrue(itemRoot.get(Item_.inArchive))));
criteriaQuery.orderBy(criteriaBuilder.asc(itemRoot.get(Item_.id)));
criteriaQuery.groupBy(itemRoot.get(Item_.id));
criteriaQuery.orderBy(criteriaBuilder.asc(itemRoot.get(DSpaceObject_.id)));
criteriaQuery.groupBy(itemRoot.get(DSpaceObject_.id));
return list(context, criteriaQuery, false, Item.class, limit, offset).iterator();
}
@Override
public int countArchivedByCollectionExcludingOwning(Context context, Collection collection) throws SQLException {
CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Item.class);
CriteriaQuery<Item> criteriaQuery = getCriteriaQuery(criteriaBuilder, Item.class);
Root<Item> itemRoot = criteriaQuery.from(Item.class);
criteriaQuery.select(itemRoot);
criteriaQuery.where(criteriaBuilder.and(

View File

@@ -23,10 +23,10 @@ import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.EntityType;
import org.dspace.content.Item;
import org.dspace.content.MetadataField;
import org.dspace.content.MetadataValue;
import org.dspace.content.Thumbnail;
import org.dspace.content.WorkspaceItem;
import org.dspace.contentreport.QueryPredicate;
import org.dspace.core.Context;
import org.dspace.discovery.SearchServiceException;
import org.dspace.eperson.EPerson;
@@ -42,7 +42,7 @@ import org.dspace.eperson.Group;
public interface ItemService
extends DSpaceObjectService<Item>, DSpaceObjectLegacySupportService<Item> {
public Thumbnail getThumbnail(Context context, Item item, boolean requireOriginal) throws SQLException;
Thumbnail getThumbnail(Context context, Item item, boolean requireOriginal) throws SQLException;
/**
* Create a new item, with a new internal ID. Authorization is done
@@ -54,7 +54,7 @@ public interface ItemService
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
public Item create(Context context, WorkspaceItem workspaceItem) throws SQLException, AuthorizeException;
Item create(Context context, WorkspaceItem workspaceItem) throws SQLException, AuthorizeException;
/**
* Create a new item, with a provided ID. Authorisation is done
@@ -67,7 +67,7 @@ public interface ItemService
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
public Item create(Context context, WorkspaceItem workspaceItem, UUID uuid) throws SQLException, AuthorizeException;
Item create(Context context, WorkspaceItem workspaceItem, UUID uuid) throws SQLException, AuthorizeException;
/**
* Create an empty template item for this collection. If one already exists,
@@ -81,7 +81,7 @@ public interface ItemService
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
public Item createTemplateItem(Context context, Collection collection) throws SQLException, AuthorizeException;
Item createTemplateItem(Context context, Collection collection) throws SQLException, AuthorizeException;
/**
* Get all the items in the archive. Only items with the "in archive" flag
@@ -91,7 +91,7 @@ public interface ItemService
* @return an iterator over the items in the archive.
* @throws SQLException if database error
*/
public Iterator<Item> findAll(Context context) throws SQLException;
Iterator<Item> findAll(Context context) throws SQLException;
/**
* Get all the items in the archive. Only items with the "in archive" flag
@@ -103,7 +103,7 @@ public interface ItemService
* @return an iterator over the items in the archive.
* @throws SQLException if database error
*/
public Iterator<Item> findAll(Context context, Integer limit, Integer offset) throws SQLException;
Iterator<Item> findAll(Context context, Integer limit, Integer offset) throws SQLException;
/**
* Get all "final" items in the archive, both archived ("in archive" flag) or
@@ -113,8 +113,7 @@ public interface ItemService
* @return an iterator over the items in the archive.
* @throws SQLException if database error
*/
@Deprecated
public Iterator<Item> findAllUnfiltered(Context context) throws SQLException;
@Deprecated Iterator<Item> findAllUnfiltered(Context context) throws SQLException;
/**
* Find all items that are:
@@ -127,7 +126,7 @@ public interface ItemService
* @return iterator over all regular items.
* @throws SQLException if database error.
*/
public Iterator<Item> findAllRegularItems(Context context) throws SQLException;
Iterator<Item> findAllRegularItems(Context context) throws SQLException;
/**
* Find all the items in the archive by a given submitter. The order is
@@ -138,7 +137,7 @@ public interface ItemService
* @return an iterator over the items submitted by eperson
* @throws SQLException if database error
*/
public Iterator<Item> findBySubmitter(Context context, EPerson eperson)
Iterator<Item> findBySubmitter(Context context, EPerson eperson)
throws SQLException;
/**
@@ -153,7 +152,7 @@ public interface ItemService
* @return an iterator over the items submitted by eperson
* @throws SQLException if database error
*/
public Iterator<Item> findBySubmitter(Context context, EPerson eperson, boolean retrieveAllItems)
Iterator<Item> findBySubmitter(Context context, EPerson eperson, boolean retrieveAllItems)
throws SQLException;
/**
@@ -165,7 +164,7 @@ public interface ItemService
* @return an iterator over the items submitted by eperson
* @throws SQLException if database error
*/
public Iterator<Item> findBySubmitterDateSorted(Context context, EPerson eperson, Integer limit)
Iterator<Item> findBySubmitterDateSorted(Context context, EPerson eperson, Integer limit)
throws SQLException;
/**
@@ -176,7 +175,7 @@ public interface ItemService
* @return an iterator over the items in the collection.
* @throws SQLException if database error
*/
public Iterator<Item> findByCollection(Context context, Collection collection) throws SQLException;
Iterator<Item> findByCollection(Context context, Collection collection) throws SQLException;
/**
* Get all the archived items in this collection. The order is indeterminate.
@@ -188,7 +187,7 @@ public interface ItemService
* @return an iterator over the items in the collection.
* @throws SQLException if database error
*/
public Iterator<Item> findByCollection(Context context, Collection collection, Integer limit, Integer offset)
Iterator<Item> findByCollection(Context context, Collection collection, Integer limit, Integer offset)
throws SQLException;
/**
@@ -201,7 +200,7 @@ public interface ItemService
* @return an iterator over the items in the collection.
* @throws SQLException if database error
*/
public Iterator<Item> findByCollectionMapping(Context context, Collection collection, Integer limit, Integer offset)
Iterator<Item> findByCollectionMapping(Context context, Collection collection, Integer limit, Integer offset)
throws SQLException;
/**
@@ -212,7 +211,7 @@ public interface ItemService
* @return an iterator over the items in the collection.
* @throws SQLException if database error
*/
public int countByCollectionMapping(Context context, Collection collection) throws SQLException;
int countByCollectionMapping(Context context, Collection collection) throws SQLException;
/**
* Get all the items (including private and withdrawn) in this collection. The order is indeterminate.
@@ -224,7 +223,7 @@ public interface ItemService
* @param offset offset value
* @throws SQLException if database error
*/
public Iterator<Item> findAllByCollection(Context context, Collection collection, Integer limit, Integer offset)
Iterator<Item> findAllByCollection(Context context, Collection collection, Integer limit, Integer offset)
throws SQLException;
/**
@@ -235,7 +234,7 @@ public interface ItemService
* @return an iterator over the items in the collection.
* @throws SQLException if database error
*/
public Iterator<Item> findInArchiveOrWithdrawnDiscoverableModifiedSince(Context context, Date since)
Iterator<Item> findInArchiveOrWithdrawnDiscoverableModifiedSince(Context context, Date since)
throws SQLException;
/**
@@ -245,7 +244,7 @@ public interface ItemService
* @return an iterator over the items in the collection.
* @throws SQLException if database error
*/
public Iterator<Item> findInArchiveOrWithdrawnNonDiscoverableModifiedSince(Context context, Date since)
Iterator<Item> findInArchiveOrWithdrawnNonDiscoverableModifiedSince(Context context, Date since)
throws SQLException;
/**
@@ -256,7 +255,7 @@ public interface ItemService
* @return an iterator over the items in the collection.
* @throws SQLException if database error
*/
public Iterator<Item> findAllByCollection(Context context, Collection collection) throws SQLException;
Iterator<Item> findAllByCollection(Context context, Collection collection) throws SQLException;
/**
* See whether this Item is contained by a given Collection.
@@ -266,7 +265,7 @@ public interface ItemService
* @return true if {@code collection} contains this Item.
* @throws SQLException if database error
*/
public boolean isIn(Item item, Collection collection) throws SQLException;
boolean isIn(Item item, Collection collection) throws SQLException;
/**
* Get the communities this item is in. Returns an unordered array of the
@@ -278,7 +277,7 @@ public interface ItemService
* @return the communities this item is in.
* @throws SQLException if database error
*/
public List<Community> getCommunities(Context context, Item item) throws SQLException;
List<Community> getCommunities(Context context, Item item) throws SQLException;
/**
@@ -289,7 +288,7 @@ public interface ItemService
* @return the bundles in an unordered array
* @throws SQLException if database error
*/
public List<Bundle> getBundles(Item item, String name) throws SQLException;
List<Bundle> getBundles(Item item, String name) throws SQLException;
/**
* Add an existing bundle to this item. This has immediate effect.
@@ -300,7 +299,7 @@ public interface ItemService
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
public void addBundle(Context context, Item item, Bundle bundle) throws SQLException, AuthorizeException;
void addBundle(Context context, Item item, Bundle bundle) throws SQLException, AuthorizeException;
/**
* Remove a bundle. This may result in the bundle being deleted, if the
@@ -313,7 +312,7 @@ public interface ItemService
* @throws AuthorizeException if authorization error
* @throws IOException if IO error
*/
public void removeBundle(Context context, Item item, Bundle bundle) throws SQLException, AuthorizeException,
void removeBundle(Context context, Item item, Bundle bundle) throws SQLException, AuthorizeException,
IOException;
/**
@@ -326,7 +325,7 @@ public interface ItemService
* @throws AuthorizeException if authorization error
* @throws IOException if IO error
*/
public void removeAllBundles(Context context, Item item) throws AuthorizeException, SQLException, IOException;
void removeAllBundles(Context context, Item item) throws AuthorizeException, SQLException, IOException;
/**
* Create a single bitstream in a new bundle. Provided as a convenience
@@ -341,7 +340,7 @@ public interface ItemService
* @throws IOException if IO error
* @throws SQLException if database error
*/
public Bitstream createSingleBitstream(Context context, InputStream is, Item item, String name)
Bitstream createSingleBitstream(Context context, InputStream is, Item item, String name)
throws AuthorizeException, IOException, SQLException;
/**
@@ -355,7 +354,7 @@ public interface ItemService
* @throws IOException if IO error
* @throws SQLException if database error
*/
public Bitstream createSingleBitstream(Context context, InputStream is, Item item)
Bitstream createSingleBitstream(Context context, InputStream is, Item item)
throws AuthorizeException, IOException, SQLException;
/**
@@ -368,7 +367,7 @@ public interface ItemService
* @return non-internal bitstreams.
* @throws SQLException if database error
*/
public List<Bitstream> getNonInternalBitstreams(Context context, Item item) throws SQLException;
List<Bitstream> getNonInternalBitstreams(Context context, Item item) throws SQLException;
/**
* Remove just the DSpace license from an item This is useful to update the
@@ -383,7 +382,7 @@ public interface ItemService
* @throws AuthorizeException if authorization error
* @throws IOException if IO error
*/
public void removeDSpaceLicense(Context context, Item item) throws SQLException, AuthorizeException,
void removeDSpaceLicense(Context context, Item item) throws SQLException, AuthorizeException,
IOException;
/**
@@ -395,7 +394,7 @@ public interface ItemService
* @throws AuthorizeException if authorization error
* @throws IOException if IO error
*/
public void removeLicenses(Context context, Item item) throws SQLException, AuthorizeException, IOException;
void removeLicenses(Context context, Item item) throws SQLException, AuthorizeException, IOException;
/**
* Withdraw the item from the archive. It is kept in place, and the content
@@ -406,7 +405,7 @@ public interface ItemService
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
public void withdraw(Context context, Item item) throws SQLException, AuthorizeException;
void withdraw(Context context, Item item) throws SQLException, AuthorizeException;
/**
@@ -417,7 +416,7 @@ public interface ItemService
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
public void reinstate(Context context, Item item) throws SQLException, AuthorizeException;
void reinstate(Context context, Item item) throws SQLException, AuthorizeException;
/**
* Return true if this Collection 'owns' this item
@@ -426,7 +425,7 @@ public interface ItemService
* @param collection Collection
* @return true if this Collection owns this item
*/
public boolean isOwningCollection(Item item, Collection collection);
boolean isOwningCollection(Item item, Collection collection);
/**
* remove all of the policies for item and replace them with a new list of
@@ -440,7 +439,7 @@ public interface ItemService
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
public void replaceAllItemPolicies(Context context, Item item, List<ResourcePolicy> newpolicies)
void replaceAllItemPolicies(Context context, Item item, List<ResourcePolicy> newpolicies)
throws SQLException,
AuthorizeException;
@@ -456,7 +455,7 @@ public interface ItemService
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
public void replaceAllBitstreamPolicies(Context context, Item item, List<ResourcePolicy> newpolicies)
void replaceAllBitstreamPolicies(Context context, Item item, List<ResourcePolicy> newpolicies)
throws SQLException, AuthorizeException;
@@ -470,7 +469,7 @@ public interface ItemService
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
public void removeGroupPolicies(Context context, Item item, Group group) throws SQLException, AuthorizeException;
void removeGroupPolicies(Context context, Item item, Group group) throws SQLException, AuthorizeException;
/**
* Remove all policies on an item and its contents, and replace them with
@@ -485,7 +484,7 @@ public interface ItemService
* draconian, but default policies must be enforced.
* @throws AuthorizeException if authorization error
*/
public void inheritCollectionDefaultPolicies(Context context, Item item, Collection collection)
void inheritCollectionDefaultPolicies(Context context, Item item, Collection collection)
throws java.sql.SQLException, AuthorizeException;
/**
@@ -504,7 +503,7 @@ public interface ItemService
* draconian, but default policies must be enforced.
* @throws AuthorizeException if authorization error
*/
public void inheritCollectionDefaultPolicies(Context context, Item item, Collection collection,
void inheritCollectionDefaultPolicies(Context context, Item item, Collection collection,
boolean overrideItemReadPolicies)
throws java.sql.SQLException, AuthorizeException;
@@ -517,14 +516,14 @@ public interface ItemService
* already applied to the bundle/bitstream. Collection's policies are inherited
* if there are no other policies defined or if the append mode is defined by
* the configuration via the core.authorization.installitem.inheritance-read.append-mode property
*
*
* @param context DSpace context object
* @param item Item to adjust policies on
* @param collection Collection
* @throws SQLException If database error
* @throws AuthorizeException If authorization error
*/
public void adjustBundleBitstreamPolicies(Context context, Item item, Collection collection)
void adjustBundleBitstreamPolicies(Context context, Item item, Collection collection)
throws SQLException, AuthorizeException;
/**
@@ -545,7 +544,7 @@ public interface ItemService
* @throws SQLException If database error
* @throws AuthorizeException If authorization error
*/
public void adjustBundleBitstreamPolicies(Context context, Item item, Collection collection,
void adjustBundleBitstreamPolicies(Context context, Item item, Collection collection,
boolean replaceReadRPWithCollectionRP)
throws SQLException, AuthorizeException;
@@ -566,7 +565,7 @@ public interface ItemService
* @throws SQLException If database error
* @throws AuthorizeException If authorization error
*/
public void adjustBitstreamPolicies(Context context, Item item, Collection collection, Bitstream bitstream)
void adjustBitstreamPolicies(Context context, Item item, Collection collection, Bitstream bitstream)
throws SQLException, AuthorizeException;
/**
@@ -588,7 +587,7 @@ public interface ItemService
* @throws SQLException If database error
* @throws AuthorizeException If authorization error
*/
public void adjustBitstreamPolicies(Context context, Item item, Collection collection, Bitstream bitstream,
void adjustBitstreamPolicies(Context context, Item item, Collection collection, Bitstream bitstream,
boolean replaceReadRPWithCollectionRP)
throws SQLException, AuthorizeException;
@@ -600,14 +599,14 @@ public interface ItemService
* inherited as appropriate. Collection's policies are inherited if there are no
* other policies defined or if the append mode is defined by the configuration
* via the core.authorization.installitem.inheritance-read.append-mode property
*
*
* @param context DSpace context object
* @param item Item to adjust policies on
* @param collection Collection
* @throws SQLException If database error
* @throws AuthorizeException If authorization error
*/
public void adjustItemPolicies(Context context, Item item, Collection collection)
void adjustItemPolicies(Context context, Item item, Collection collection)
throws SQLException, AuthorizeException;
/**
@@ -626,7 +625,7 @@ public interface ItemService
* @throws SQLException If database error
* @throws AuthorizeException If authorization error
*/
public void adjustItemPolicies(Context context, Item item, Collection collection,
void adjustItemPolicies(Context context, Item item, Collection collection,
boolean replaceReadRPWithCollectionRP)
throws SQLException, AuthorizeException;
@@ -641,7 +640,7 @@ public interface ItemService
* @throws AuthorizeException if authorization error
* @throws IOException if IO error
*/
public void move(Context context, Item item, Collection from, Collection to)
void move(Context context, Item item, Collection from, Collection to)
throws SQLException, AuthorizeException, IOException;
/**
@@ -656,7 +655,7 @@ public interface ItemService
* @throws AuthorizeException if authorization error
* @throws IOException if IO error
*/
public void move(Context context, Item item, Collection from, Collection to, boolean inheritDefaultPolicies)
void move(Context context, Item item, Collection from, Collection to, boolean inheritDefaultPolicies)
throws SQLException, AuthorizeException, IOException;
/**
@@ -667,7 +666,7 @@ public interface ItemService
* bitstreams inside
* @throws SQLException if database error
*/
public boolean hasUploadedFiles(Item item) throws SQLException;
boolean hasUploadedFiles(Item item) throws SQLException;
/**
* Get the collections this item is not in.
@@ -677,7 +676,7 @@ public interface ItemService
* @return the collections this item is not in, if any.
* @throws SQLException if database error
*/
public List<Collection> getCollectionsNotLinked(Context context, Item item) throws SQLException;
List<Collection> getCollectionsNotLinked(Context context, Item item) throws SQLException;
/**
* return TRUE if context's user can edit item, false otherwise
@@ -687,7 +686,7 @@ public interface ItemService
* @return boolean true = current user can edit item
* @throws SQLException if database error
*/
public boolean canEdit(Context context, Item item) throws java.sql.SQLException;
boolean canEdit(Context context, Item item) throws java.sql.SQLException;
/**
* return TRUE if context's user can create new version of the item, false
@@ -698,7 +697,7 @@ public interface ItemService
* @return boolean true = current user can create new version of the item
* @throws SQLException if database error
*/
public boolean canCreateNewVersion(Context context, Item item) throws SQLException;
boolean canCreateNewVersion(Context context, Item item) throws SQLException;
/**
* Returns an iterator of in archive items possessing the passed metadata field, or only
@@ -713,7 +712,7 @@ public interface ItemService
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
public Iterator<Item> findArchivedByMetadataField(Context context, String schema,
Iterator<Item> findArchivedByMetadataField(Context context, String schema,
String element, String qualifier,
String value) throws SQLException, AuthorizeException;
@@ -728,7 +727,7 @@ public interface ItemService
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
public Iterator<Item> findArchivedByMetadataField(Context context, String metadataField, String value)
Iterator<Item> findArchivedByMetadataField(Context context, String metadataField, String value)
throws SQLException, AuthorizeException;
/**
@@ -745,14 +744,41 @@ public interface ItemService
* @throws AuthorizeException if authorization error
* @throws IOException if IO error
*/
public Iterator<Item> findByMetadataField(Context context,
Iterator<Item> findByMetadataField(Context context,
String schema, String element, String qualifier, String value)
throws SQLException, AuthorizeException, IOException;
public Iterator<Item> findByMetadataQuery(Context context, List<List<MetadataField>> listFieldList,
List<String> query_op, List<String> query_val, List<UUID> collectionUuids,
String regexClause, int offset, int limit)
throws SQLException, AuthorizeException, IOException;
/**
* Returns a list of items that match the given predicates, within the
* specified collections, if any. This querying method is used by the
* Filtered Items report functionality.
* @param context DSpace context object
* @param queryPredicates metadata field predicates
* @param collectionUuids UUIDs of the collections to search
* @param offset position in the list to start returning items
* @param limit maximum number of items to return
* @return a list of matching items in the specified collections,
* or in any collection if no collection UUIDs are provided
* @throws SQLException if a database error occurs
*/
List<Item> findByMetadataQuery(Context context, List<QueryPredicate> queryPredicates,
List<UUID> collectionUuids, long offset, int limit)
throws SQLException;
/**
* Returns the total number of items that match the given predicates, within the
* specified collections, if any. This querying method is used for pagination by the
* Filtered Items report functionality.
* @param context DSpace context object
* @param queryPredicates metadata field predicates
* @param collectionUuids UUIDs of the collections to search
* @return the total number of matching items in the specified collections,
* or in any collection if no collection UUIDs are provided
* @throws SQLException if a database error occurs
*/
long countForMetadataQuery(Context context, List<QueryPredicate> queryPredicates,
List<UUID> collectionUuids)
throws SQLException;
/**
* Find all the items in the archive with a given authority key value
@@ -768,12 +794,12 @@ public interface ItemService
* @throws AuthorizeException if authorization error
* @throws IOException if IO error
*/
public Iterator<Item> findByAuthorityValue(Context context,
Iterator<Item> findByAuthorityValue(Context context,
String schema, String element, String qualifier, String value)
throws SQLException, AuthorizeException;
public Iterator<Item> findByMetadataFieldAuthority(Context context, String mdString, String authority)
Iterator<Item> findByMetadataFieldAuthority(Context context, String mdString, String authority)
throws SQLException, AuthorizeException;
/**
@@ -785,7 +811,7 @@ public interface ItemService
* @param item item
* @return true or false
*/
public boolean isItemListedForUser(Context context, Item item);
boolean isItemListedForUser(Context context, Item item);
/**
* counts items in the given collection
@@ -795,7 +821,7 @@ public interface ItemService
* @return total items
* @throws SQLException if database error
*/
public int countItems(Context context, Collection collection) throws SQLException;
int countItems(Context context, Collection collection) throws SQLException;
/**
* counts all items in the given collection including withdrawn items
@@ -805,7 +831,7 @@ public interface ItemService
* @return total items
* @throws SQLException if database error
*/
public int countAllItems(Context context, Collection collection) throws SQLException;
int countAllItems(Context context, Collection collection) throws SQLException;
/**
* Find all Items modified since a Date.
@@ -815,7 +841,7 @@ public interface ItemService
* @return iterator over items
* @throws SQLException if database error
*/
public Iterator<Item> findByLastModifiedSince(Context context, Date last)
Iterator<Item> findByLastModifiedSince(Context context, Date last)
throws SQLException;
/**
@@ -826,7 +852,7 @@ public interface ItemService
* @return total items
* @throws SQLException if database error
*/
public int countItems(Context context, Community community) throws SQLException;
int countItems(Context context, Community community) throws SQLException;
/**
* counts all items in the given community including withdrawn
@@ -836,7 +862,7 @@ public interface ItemService
* @return total items
* @throws SQLException if database error
*/
public int countAllItems(Context context, Community community) throws SQLException;
int countAllItems(Context context, Community community) throws SQLException;
/**
* counts all items
@@ -883,7 +909,7 @@ public interface ItemService
* @throws SQLException
* @throws SearchServiceException
*/
public List<Item> findItemsWithEdit(Context context, int offset, int limit)
List<Item> findItemsWithEdit(Context context, int offset, int limit)
throws SQLException, SearchServiceException;
/**
@@ -893,7 +919,7 @@ public interface ItemService
* @throws SQLException
* @throws SearchServiceException
*/
public int countItemsWithEdit(Context context) throws SQLException, SearchServiceException;
int countItemsWithEdit(Context context) throws SQLException, SearchServiceException;
/**
* Check if the supplied item is an inprogress submission
@@ -953,7 +979,7 @@ public interface ItemService
* relationships.
* @return metadata fields that match the parameters
*/
public List<MetadataValue> getMetadata(Item item, String schema, String element, String qualifier,
List<MetadataValue> getMetadata(Item item, String schema, String element, String qualifier,
String lang, boolean enableVirtualMetadata);
/**
@@ -961,7 +987,7 @@ public interface ItemService
* @param item the item.
* @return the label of the entity type, taken from the item metadata, or null if not found.
*/
public String getEntityTypeLabel(Item item);
String getEntityTypeLabel(Item item);
/**
* Retrieve the entity type of the given item.
@@ -969,6 +995,6 @@ public interface ItemService
* @param item the item.
* @return the entity type of the given item, or null if not found.
*/
public EntityType getEntityType(Context context, Item item) throws SQLException;
EntityType getEntityType(Context context, Item item) throws SQLException;
}

View File

@@ -0,0 +1,190 @@
/**
* 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.contentreport;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import org.apache.logging.log4j.Logger;
import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.Item;
import org.dspace.content.MetadataField;
import org.dspace.content.service.CollectionService;
import org.dspace.content.service.ItemService;
import org.dspace.content.service.MetadataFieldService;
import org.dspace.contentreport.service.ContentReportService;
import org.dspace.core.Context;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
public class ContentReportServiceImpl implements ContentReportService {
private static final Logger log = org.apache.logging.log4j.LogManager
.getLogger(ContentReportServiceImpl.class);
@Autowired
protected ConfigurationService configurationService;
@Autowired
private CollectionService collectionService;
@Autowired
private ItemService itemService;
@Autowired
private MetadataFieldService metadataFieldService;
/**
* Returns <code>true<</code> if Content Reports are enabled.
* @return <code>true<</code> if Content Reports are enabled
*/
@Override
public boolean getEnabled() {
return configurationService.getBooleanProperty("contentreport.enable");
}
/**
* Retrieves item statistics per collection according to a set of Boolean filters.
* @param context DSpace context
* @param filters Set of filters
* @return a list of collections with the requested statistics for each of them
*/
@Override
public List<FilteredCollection> findFilteredCollections(Context context, java.util.Collection<Filter> filters) {
List<FilteredCollection> colls = new ArrayList<>();
try {
List<Collection> collections = collectionService.findAll(context);
for (Collection collection : collections) {
FilteredCollection coll = new FilteredCollection();
coll.setHandle(collection.getHandle());
coll.setLabel(collection.getName());
Community community = collection.getCommunities().stream()
.findFirst()
.orElse(null);
if (community != null) {
coll.setCommunityLabel(community.getName());
coll.setCommunityHandle(community.getHandle());
}
colls.add(coll);
Iterator<Item> items = itemService.findAllByCollection(context, collection);
int nbTotalItems = 0;
while (items.hasNext()) {
Item item = items.next();
nbTotalItems++;
boolean matchesAllFilters = true;
for (Filter filter : filters) {
if (filter.testItem(context, item)) {
coll.addValue(filter, 1);
} else {
// This ensures the requested filter is present in the collection record
// even when there are no matching items.
coll.addValue(filter, 0);
matchesAllFilters = false;
}
}
if (matchesAllFilters) {
coll.addAllFiltersValue(1);
}
}
coll.setTotalItems(nbTotalItems);
coll.seal();
}
} catch (SQLException e) {
log.error("SQLException trying to receive filtered collections statistics", e);
}
return colls;
}
/**
* Retrieves a list of items according to a set of criteria.
* @param context DSpace context
* @param query structured query to find items against
* @return a list of items filtered according to the provided query
*/
@Override
public FilteredItems findFilteredItems(Context context, FilteredItemsQuery query) {
FilteredItems report = new FilteredItems();
List<QueryPredicate> predicates = query.getQueryPredicates();
List<UUID> collectionUuids = getUuidsFromStrings(query.getCollections());
Set<Filter> filters = query.getFilters();
try {
List<Item> items = itemService.findByMetadataQuery(context, predicates, collectionUuids,
query.getOffset(), query.getPageLimit());
items.stream()
.filter(item -> filters.stream().allMatch(f -> f.testItem(context, item)))
.forEach(report::addItem);
} catch (SQLException e) {
log.error(e.getMessage(), e);
}
try {
long count = itemService.countForMetadataQuery(context, predicates, collectionUuids);
report.setItemCount(count);
} catch (SQLException e) {
log.error(e.getMessage(), e);
}
return report;
}
/**
* Converts a metadata field name to a list of {@link MetadataField} instances
* (one if no wildcards are used, possibly more otherwise).
* @param context DSpace context
* @param metadataField field to search for
* @return a corresponding list of {@link MetadataField} entries
*/
@Override
public List<MetadataField> getMetadataFields(org.dspace.core.Context context, String metadataField)
throws SQLException {
List<MetadataField> fields = new ArrayList<>();
if ("*".equals(metadataField)) {
return fields;
}
String schema = "";
String element = "";
String qualifier = null;
String[] parts = metadataField.split("\\.");
if (parts.length > 0) {
schema = parts[0];
}
if (parts.length > 1) {
element = parts[1];
}
if (parts.length > 2) {
qualifier = parts[2];
}
if (Item.ANY.equals(qualifier)) {
fields.addAll(metadataFieldService.findFieldsByElementNameUnqualified(context, schema, element));
} else {
MetadataField mf = metadataFieldService.findByElement(context, schema, element, qualifier);
if (mf != null) {
fields.add(mf);
}
}
return fields;
}
private static List<UUID> getUuidsFromStrings(List<String> collSel) {
List<UUID> uuids = new ArrayList<>();
for (String s: collSel) {
try {
uuids.add(UUID.fromString(s));
} catch (IllegalArgumentException e) {
log.warn("Invalid collection UUID: " + s);
}
}
return uuids;
}
}

View File

@@ -0,0 +1,399 @@
/**
* 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.contentreport;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.authorize.factory.AuthorizeServiceFactory;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.Bundle;
import org.dspace.content.Item;
import org.dspace.contentreport.ItemFilterUtil.BundleName;
import org.dspace.core.Context;
import org.dspace.services.factory.DSpaceServicesFactory;
/**
* Available filters for the Filtered Collections and Filtered Items reports.
* In this enum, each item corresponds to a separate property, not values of
* a single property, hence the @JsonProperty applied to each of them.
* For each item, the annotation value is read through reflection and copied into
* the id property, which eliminates repetitions, hence reducing the risk or errors.
*
* @author Jean-François Morin (Université Laval)
*/
public enum Filter {
@JsonProperty("is_item")
IS_ITEM(FilterCategory.PROPERTY, (context, item) -> true),
@JsonProperty("is_withdrawn")
IS_WITHDRAWN(FilterCategory.PROPERTY, (context, item) -> item.isWithdrawn()),
@JsonProperty("is_not_withdrawn")
IS_NOT_WITHDRAWN(FilterCategory.PROPERTY, (context, item) -> !item.isWithdrawn()),
@JsonProperty("is_discoverable")
IS_DISCOVERABLE(FilterCategory.PROPERTY, (context, item) -> item.isDiscoverable()),
@JsonProperty("is_not_discoverable")
IS_NOT_DISCOVERABLE(FilterCategory.PROPERTY, (context, item) -> !item.isDiscoverable()),
/**
* Matches items having multiple original bitstreams.
*/
@JsonProperty("has_multiple_originals")
HAS_MULTIPLE_ORIGINALS(FilterCategory.BITSTREAM, (context, item) ->
ItemFilterUtil.countOriginalBitstream(item) > 1),
/**
* Matches items having no original bitstreams.
*/
@JsonProperty("has_no_originals")
HAS_NO_ORIGINALS(FilterCategory.BITSTREAM, (context, item) -> ItemFilterUtil.countOriginalBitstream(item) == 0),
/**
* Matches items having exactly one original bitstream.
*/
@JsonProperty("has_one_original")
HAS_ONE_ORIGINAL(FilterCategory.BITSTREAM, (context, item) -> ItemFilterUtil.countOriginalBitstream(item) == 1),
/**
* Matches items having bitstreams with a MIME type that matches one defined in the "rest.report-mime-document"
* configuration property.
*/
@JsonProperty("has_doc_original")
HAS_DOC_ORIGINAL(FilterCategory.BITSTREAM_MIME, (context, item) ->
ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.getDocumentMimeTypes()) > 0),
/**
* Matches items having bitstreams with a MIME type starting with "image" (e.g., image/jpeg, image/png).
*/
@JsonProperty("has_image_original")
HAS_IMAGE_ORIGINAL(FilterCategory.BITSTREAM_MIME, (context, item) ->
ItemFilterUtil.countOriginalBitstreamMimeStartsWith(context, item, "image") > 0),
/**
* Matches items having bitstreams with a MIME type other than document (cf. HAS_DOCUMENT above) or image
* (cf. HAS_IMAGE_ORIGINAL above).
*/
@JsonProperty("has_unsupp_type")
HAS_UNSUPPORTED_TYPE(FilterCategory.BITSTREAM_MIME, (context, item) -> {
int bitCount = ItemFilterUtil.countOriginalBitstream(item);
if (bitCount == 0) {
return false;
}
int docCount = ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.getDocumentMimeTypes());
int imgCount = ItemFilterUtil.countOriginalBitstreamMimeStartsWith(context, item, "image");
return (bitCount - docCount - imgCount) > 0;
}),
/**
* Matches items having bitstreams of multiple types (document, image, other).
*/
@JsonProperty("has_mixed_original")
HAS_MIXED_ORIGINAL(FilterCategory.BITSTREAM_MIME, (context, item) -> {
int countBit = ItemFilterUtil.countOriginalBitstream(item);
if (countBit <= 1) {
return false;
}
int countDoc = ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.getDocumentMimeTypes());
if (countDoc > 0) {
return countDoc != countBit;
}
int countImg = ItemFilterUtil.countOriginalBitstreamMimeStartsWith(context, item, "image");
if (countImg > 0) {
return countImg != countBit;
}
return false;
}),
@JsonProperty("has_pdf_original")
HAS_PDF_ORIGINAL(FilterCategory.BITSTREAM_MIME, (context, item) ->
ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.MIMES_PDF) > 0),
@JsonProperty("has_jpg_original")
HAS_JPEG_ORIGINAL(FilterCategory.BITSTREAM_MIME, (context, item) ->
ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.MIMES_JPG) > 0),
/**
* Matches items having at least one PDF of size less than 20 kb (configurable in rest.cfg).
*/
@JsonProperty("has_small_pdf")
HAS_SMALL_PDF(FilterCategory.BITSTREAM_MIME, (context, item) ->
ItemFilterUtil.countBitstreamSmallerThanMinSize(
context, BundleName.ORIGINAL, item, ItemFilterUtil.MIMES_PDF, "rest.report-pdf-min-size") > 0),
/**
* Matches items having at least one PDF of size more than 25 Mb (configurable in rest.cfg).
*/
@JsonProperty("has_large_pdf")
HAS_LARGE_PDF(FilterCategory.BITSTREAM_MIME, (context, item) ->
ItemFilterUtil.countBitstreamLargerThanMaxSize(
context, BundleName.ORIGINAL, item, ItemFilterUtil.MIMES_PDF, "rest.report-pdf-max-size") > 0),
/**
* Matches items having at least one non-text bitstream.
*/
@JsonProperty("has_doc_without_text")
HAS_DOC_WITHOUT_TEXT(FilterCategory.BITSTREAM_MIME, (context, item) -> {
int countDoc = ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.getDocumentMimeTypes());
if (countDoc == 0) {
return false;
}
int countText = ItemFilterUtil.countBitstream(BundleName.TEXT, item);
return countDoc > countText;
}),
/**
* Matches items having at least one image, but all of supported types.
*/
@JsonProperty("has_only_supp_image_type")
HAS_ONLY_SUPPORTED_IMAGE_TYPE(FilterCategory.MIME, (context, item) -> {
int imageCount = ItemFilterUtil.countOriginalBitstreamMimeStartsWith(context, item, "image/");
if (imageCount == 0) {
return false;
}
int suppImageCount = ItemFilterUtil.countOriginalBitstreamMime(
context, item, ItemFilterUtil.getSupportedImageMimeTypes());
return (imageCount == suppImageCount);
}),
/**
* Matches items having at least one image of an unsupported type.
*/
@JsonProperty("has_unsupp_image_type")
HAS_UNSUPPORTED_IMAGE_TYPE(FilterCategory.MIME, (context, item) -> {
int imageCount = ItemFilterUtil.countOriginalBitstreamMimeStartsWith(context, item, "image/");
if (imageCount == 0) {
return false;
}
int suppImageCount = ItemFilterUtil.countOriginalBitstreamMime(
context, item, ItemFilterUtil.getSupportedImageMimeTypes());
return (imageCount - suppImageCount) > 0;
}),
/**
* Matches items having at least one document, but all of supported types.
*/
@JsonProperty("has_only_supp_doc_type")
HAS_ONLY_SUPPORTED_DOC_TYPE(FilterCategory.MIME, (context, item) -> {
int docCount = ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.getDocumentMimeTypes());
if (docCount == 0) {
return false;
}
int suppDocCount = ItemFilterUtil.countOriginalBitstreamMime(
context, item, ItemFilterUtil.getSupportedDocumentMimeTypes());
return docCount == suppDocCount;
}),
/**
* Matches items having at least one document of an unsupported type.
*/
@JsonProperty("has_unsupp_doc_type")
HAS_UNSUPPORTED_DOC_TYPE(FilterCategory.MIME, (context, item) -> {
int docCount = ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.getDocumentMimeTypes());
if (docCount == 0) {
return false;
}
int suppDocCount = ItemFilterUtil.countOriginalBitstreamMime(
context, item, ItemFilterUtil.getSupportedDocumentMimeTypes());
return (docCount - suppDocCount) > 0;
}),
/**
* Matches items having at least one unsupported bundle.
*/
@JsonProperty("has_unsupported_bundle")
HAS_UNSUPPORTED_BUNDLE(FilterCategory.BUNDLE, (context, item) -> {
String[] bundleList = DSpaceServicesFactory.getInstance().getConfigurationService()
.getArrayProperty("rest.report-supp-bundles");
return ItemFilterUtil.hasUnsupportedBundle(item, bundleList);
}),
/**
* Matches items having at least one thumbnail of size less than 400 bytes (configurable in rest.cfg).
*/
@JsonProperty("has_small_thumbnail")
HAS_SMALL_THUMBNAIL(FilterCategory.BUNDLE, (context, item) ->
ItemFilterUtil.countBitstreamSmallerThanMinSize(
context, BundleName.THUMBNAIL, item, ItemFilterUtil.MIMES_JPG, "rest.report-thumbnail-min-size") > 0),
/**
* Matches items having at least one original without a thumbnail.
*/
@JsonProperty("has_original_without_thumbnail")
HAS_ORIGINAL_WITHOUT_THUMBNAIL(FilterCategory.BUNDLE, (context, item) -> {
int countBit = ItemFilterUtil.countOriginalBitstream(item);
if (countBit == 0) {
return false;
}
int countThumb = ItemFilterUtil.countBitstream(BundleName.THUMBNAIL, item);
return countBit > countThumb;
}),
/**
* Matches items having at least one non-JPEG thumbnail.
*/
@JsonProperty("has_invalid_thumbnail_name")
HAS_INVALID_THUMBNAIL_NAME(FilterCategory.BUNDLE, (context, item) -> {
List<String> originalNames = ItemFilterUtil.getBitstreamNames(BundleName.ORIGINAL, item);
List<String> thumbNames = ItemFilterUtil.getBitstreamNames(BundleName.THUMBNAIL, item);
if (thumbNames.size() != originalNames.size()) {
return false;
}
return originalNames.stream()
.anyMatch(name -> !thumbNames.contains(name + ".jpg") && !thumbNames.contains(name + ".jpeg"));
}),
/**
* Matches items having at least one non-generated thumbnail.
*/
@JsonProperty("has_non_generated_thumb")
HAS_NON_GENERATED_THUMBNAIL(FilterCategory.BUNDLE, (context, item) -> {
String[] generatedThumbDesc = DSpaceServicesFactory.getInstance().getConfigurationService()
.getArrayProperty("rest.report-gen-thumbnail-desc");
int countThumb = ItemFilterUtil.countBitstream(BundleName.THUMBNAIL, item);
if (countThumb == 0) {
return false;
}
int countGen = ItemFilterUtil.countBitstreamByDesc(BundleName.THUMBNAIL, item, generatedThumbDesc);
return (countThumb > countGen);
}),
/**
* Matches items having no licence-typed bitstreams.
*/
@JsonProperty("no_license")
NO_LICENSE(FilterCategory.BUNDLE, (context, item) ->
ItemFilterUtil.countBitstream(BundleName.LICENSE, item) == 0),
/**
* Matches items having licence documentation (a licence bitstream named other than license.txt).
*/
@JsonProperty("has_license_documentation")
HAS_LICENSE_DOCUMENTATION(FilterCategory.BUNDLE, (context, item) -> {
List<String> names = ItemFilterUtil.getBitstreamNames(BundleName.LICENSE, item);
return names.stream()
.anyMatch(name -> !name.equals("license.txt"));
}),
/**
* Matches items having at least one original with restricted access.
*/
@JsonProperty("has_restricted_original")
HAS_RESTRICTED_ORIGINAL(FilterCategory.PERMISSION, (context, item) -> {
return item.getBundles().stream()
.filter(bundle -> bundle.getName().equals(BundleName.ORIGINAL.name()))
.map(Bundle::getBitstreams)
.flatMap(List::stream)
.anyMatch(bit -> {
try {
if (!getAuthorizeService()
.authorizeActionBoolean(getAnonymousContext(), bit, org.dspace.core.Constants.READ)) {
return true;
}
} catch (SQLException e) {
getLog().warn("SQL Exception testing original bitstream access " + e.getMessage(), e);
}
return false;
});
}),
/**
* Matches items having at least one thumbnail with restricted access.
*/
@JsonProperty("has_restricted_thumbnail")
HAS_RESTRICTED_THUMBNAIL(FilterCategory.PERMISSION, (context, item) -> {
return item.getBundles().stream()
.filter(bundle -> bundle.getName().equals(BundleName.THUMBNAIL.name()))
.map(Bundle::getBitstreams)
.flatMap(List::stream)
.anyMatch(bit -> {
try {
if (!getAuthorizeService()
.authorizeActionBoolean(getAnonymousContext(), bit, org.dspace.core.Constants.READ)) {
return true;
}
} catch (SQLException e) {
getLog().warn("SQL Exception testing thumbnail bitstream access " + e.getMessage(), e);
}
return false;
});
}),
/**
* Matches items having metadata with restricted access.
*/
@JsonProperty("has_restricted_metadata")
HAS_RESTRICTED_METADATA(FilterCategory.PERMISSION, (context, item) -> {
try {
return !getAuthorizeService()
.authorizeActionBoolean(getAnonymousContext(), item, org.dspace.core.Constants.READ);
} catch (SQLException e) {
getLog().warn("SQL Exception testing item metadata access " + e.getMessage(), e);
return false;
}
});
private static final Logger log = LogManager.getLogger();
private static AuthorizeService authorizeService;
private static Context anonymousContext;
private String id;
private FilterCategory category;
private BiPredicate<Context, Item> itemTester;
Filter(FilterCategory category, BiPredicate<Context, Item> itemTester) {
try {
JsonProperty jp = getClass().getField(name()).getAnnotation(JsonProperty.class);
id = Optional.ofNullable(jp).map(JsonProperty::value).orElse(name());
} catch (Exception e) {
id = name();
}
this.category = category;
this.itemTester = itemTester;
}
public String getId() {
return id;
}
public FilterCategory getCategory() {
return category;
}
public boolean testItem(Context context, Item item) {
return itemTester.test(context, item);
}
private static Logger getLog() {
return log;
}
private static AuthorizeService getAuthorizeService() {
if (authorizeService == null) {
authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService();
}
return authorizeService;
}
private static Context getAnonymousContext() {
if (anonymousContext == null) {
anonymousContext = new Context();
}
return anonymousContext;
}
@JsonCreator
public static Filter get(String id) {
return Arrays.stream(values())
.filter(item -> Objects.equals(item.id, id))
.findFirst()
.orElse(null);
}
public static Set<Filter> getFilters(String filters) {
String[] ids = Optional.ofNullable(filters).orElse("").split("[^a-z_]+");
Set<Filter> set = Arrays.stream(ids)
.map(Filter::get)
.filter(f -> f != null)
.collect(Collectors.toCollection(() -> EnumSet.noneOf(Filter.class)));
if (set == null) {
set = EnumSet.noneOf(Filter.class);
}
return set;
}
}

View File

@@ -0,0 +1,50 @@
/**
* 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.contentreport;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* Identifies the category/section of filters defined in the {@link Filter} enum.
* This enum will be used when/if the structured filter definitions are returned to
* the Angular layer through a REST endpoint.
*
* @author Jean-François Morin (Université Laval)
*/
public enum FilterCategory {
PROPERTY("property"),
BITSTREAM("bitstream"),
BITSTREAM_MIME("bitstream_mime"),
MIME("mime"),
BUNDLE("bundle"),
PERMISSION("permission");
private String id;
private List<Filter> filters;
FilterCategory(String id) {
this.id = id;
}
public String getId() {
return id;
}
public List<Filter> getFilters() {
if (filters == null) {
filters = Arrays.stream(Filter.values())
.filter(f -> f.getCategory() == this)
.collect(Collectors.toUnmodifiableList());
}
return filters;
}
}

View File

@@ -0,0 +1,219 @@
/**
* 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.contentreport;
import java.io.Serializable;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
/**
* This class represents an entry in the Filtered Collections report.
*
* @author Jean-François Morin (Université Laval)
*/
public class FilteredCollection implements Cloneable, Serializable {
private static final long serialVersionUID = -231735620268582719L;
/** Name of the collection */
private String label;
/** Handle of the collection, used to make it clickable from the generated report */
private String handle;
/** Name of the owning community */
private String communityLabel;
/** Handle of the owning community, used to make it clickable from the generated report */
private String communityHandle;
/** Total number of items in the collection */
private int totalItems;
/** Number of filtered items per requested filter in the collection */
private Map<Filter, Integer> values = new EnumMap<>(Filter.class);
/** Number of items in the collection that match all requested filters */
private int allFiltersValue;
/**
* Indicates whether this object is protected against further changes.
* This is used in computing summary data in the parent FilteredCollectionsRest class.
*/
private boolean sealed;
/**
* Shortcut method that builds a FilteredCollectionRest instance
* from its building blocks.
* @param label Name of the collection
* @param handle Handle of the collection
* @param communityLabel Name of the owning community
* @param communityHandle Handle of the owning community
* @param totalItems Total number of items in the collection
* @param allFiltersValue Number of items in the collection that match all requested filters
* @param values Number of filtered items per requested filter in the collection
* @param doSeal true if the collection must be sealed immediately
* @return a FilteredCollectionRest instance built from the provided parameters
*/
public static FilteredCollection of(String label, String handle,
String communityLabel, String communityHandle,
int totalItems, int allFiltersValue, Map<Filter, Integer> values, boolean doSeal) {
var coll = new FilteredCollection();
coll.label = label;
coll.handle = handle;
coll.communityLabel = communityLabel;
coll.communityHandle = communityHandle;
coll.totalItems = totalItems;
coll.allFiltersValue = allFiltersValue;
Optional.ofNullable(values).ifPresent(vs -> vs.forEach(coll::addValue));
if (doSeal) {
coll.seal();
}
return coll;
}
/**
* Returns the item counts per filter.
* If this object is sealed, a defensive copy will be returned.
*
* @return the item counts per filter
*/
public Map<Filter, Integer> getValues() {
if (sealed) {
return new EnumMap<>(values);
}
return values;
}
/**
* Increments a filtered item count for a given filter.
*
* @param filter Filter to add to the requested filters in this collection
* @param delta Number by which the filtered item count must be incremented
* for the requested filter
*/
public void addValue(Filter filter, int delta) {
checkSealed();
Integer oldValue = values.getOrDefault(filter, Integer.valueOf(0));
int newValue = oldValue.intValue() + delta;
values.put(filter, Integer.valueOf(newValue));
}
/**
* Sets all filtered item counts for this collection.
* The contents are copied into this object's internal Map, which is protected against
* further tampering with the provided Map.
*
* @param values Values that replace the current ones
*/
public void setValues(Map<? extends Filter, ? extends Integer> values) {
checkSealed();
this.values.clear();
this.values.putAll(values);
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
checkSealed();
this.label = label;
}
public String getHandle() {
return handle;
}
public void setHandle(String handle) {
checkSealed();
this.handle = handle;
}
public String getCommunityLabel() {
return communityLabel;
}
public void setCommunityLabel(String communityLabel) {
checkSealed();
this.communityLabel = communityLabel;
}
public String getCommunityHandle() {
return communityHandle;
}
public void setCommunityHandle(String communityHandle) {
checkSealed();
this.communityHandle = communityHandle;
}
public int getTotalItems() {
return totalItems;
}
public void setTotalItems(int totalItems) {
checkSealed();
this.totalItems = totalItems;
}
public int getAllFiltersValue() {
return allFiltersValue;
}
/**
* Increments the count of items matching all filters.
*
* @param delta Number by which the count must be incremented
*/
public void addAllFiltersValue(int delta) {
checkSealed();
allFiltersValue++;
}
/**
* Replaces the count of items matching all filters.
*
* @param allFiltersValue Number that replaces the current item count
*/
public void setAllFiltersValue(int allFiltersValue) {
checkSealed();
this.allFiltersValue = allFiltersValue;
}
public boolean getSealed() {
return sealed;
}
/**
* Seals this filtered collection object.
* No changes to this object can be made afterwards. Any attempt will throw
* an IllegalStateException.
*/
public void seal() {
sealed = true;
}
private void checkSealed() {
if (sealed) {
throw new IllegalStateException("This filtered collection record is sealed"
+ " and cannot be modified anymore. You can apply changes to a non-sealed clone.");
}
}
/**
* Returns a non-sealed clone of this filtered collection record.
*
* @return a new non-sealed FilteredCollectionRest instance containing
* all attribute values of this object
*/
@Override
public FilteredCollection clone() {
var clone = new FilteredCollection();
clone.label = label;
clone.handle = handle;
clone.values.putAll(values);
clone.allFiltersValue = allFiltersValue;
return clone;
}
}

View File

@@ -0,0 +1,107 @@
/**
* 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.contentreport;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
/**
* This class represents the complete result of a Filtered Collections report query.
* In addition to the list of FilteredCollection entries, it contains the lazily computed
* summary to be included in the completed report.
*
* @author Jean-François Morin (Université Laval)
*/
public class FilteredCollections implements Serializable {
private static final long serialVersionUID = 3622651208704009095L;
/** Collections included in the report */
private List<FilteredCollection> collections = new ArrayList<>();
/**
* Summary generated by adding up data for each filter included in the report.
* It will be regenerated if any non-sealed collection item is found in
* the {@link #collections} collection attribute.
*/
private FilteredCollection summary;
/**
* Shortcut method that builds a FilteredCollectionsRest instance
* from its building blocks.
* @param collections a list of FilteredCollectionRest instances
* @return a FilteredCollectionsRest instance built from the provided parameters
*/
public static FilteredCollections of(Collection<FilteredCollection> collections) {
var colls = new FilteredCollections();
Optional.ofNullable(collections).ifPresent(cs -> cs.stream().forEach(colls::addCollection));
return colls;
}
/**
* Returns a defensive copy of the collections included in this report.
*
* @return the collections included in this report
*/
public List<FilteredCollection> getCollections() {
return new ArrayList<>(collections);
}
/**
* Adds a {@link FilteredCollectionRest} object to this report.
*
* @param coll {@link FilteredCollectionRest} to add to this report
*/
public void addCollection(FilteredCollection coll) {
summary = null;
collections.add(coll);
}
/**
* Sets all collections for this report.
* The contents are copied into this object's internal list, which is protected against
* further tampering with the provided list.
*
* @param collections Values that replace the current ones
*/
public void setCollections(List<FilteredCollection> collections) {
summary = null;
this.collections.clear();
this.collections.addAll(collections);
}
/**
* Returns the report summary.
* If the summary has not been computed yet and/or the report includes non-sealed collections,
* it will be regenerated.
*
* @return the generated report summary
*/
public FilteredCollection getSummary() {
boolean needsRefresh = summary == null || collections.stream().anyMatch(c -> !c.getSealed());
if (needsRefresh) {
summary = new FilteredCollection();
for (var coll : collections) {
coll.getValues().forEach(summary::addValue);
}
int total = collections.stream()
.mapToInt(FilteredCollection::getTotalItems)
.sum();
summary.setTotalItems(total);
int allFilters = collections.stream()
.mapToInt(FilteredCollection::getAllFiltersValue)
.sum();
summary.setAllFiltersValue(allFilters);
summary.seal();
}
return summary;
}
}

View File

@@ -0,0 +1,70 @@
/**
* 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.contentreport;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.dspace.content.Item;
/**
* This class represents a list of items for a Filtered Items report query.
* Since the underlying list should correspond to only a page of results,
* the total number of items found through the query is included in this report.
*
* @author Jean-François Morin (Université Laval)
*/
public class FilteredItems implements Serializable {
private static final long serialVersionUID = 7980375013177658249L;
/** Items included in the report */
private List<Item> items = new ArrayList<>();
/** Total item count (for pagination) */
private long itemCount;
/**
* Returns a defensive copy of the items included in this report.
*
* @return the items included in this report
*/
public List<Item> getItems() {
return new ArrayList<>(items);
}
/**
* Adds an {@link ItemRest} object to this report.
*
* @param item {@link ItemRest} to add to this report
*/
public void addItem(Item item) {
items.add(item);
}
/**
* Sets all items for this report.
* The contents are copied into this object's internal list, which is protected
* against further tampering with the provided list.
*
* @param items Values that replace the current ones
*/
public void setItems(List<Item> items) {
this.items.clear();
this.items.addAll(items);
}
public long getItemCount() {
return itemCount;
}
public void setItemCount(long itemCount) {
this.itemCount = itemCount;
}
}

View File

@@ -0,0 +1,115 @@
/**
* 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.contentreport;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
* Structured query contents for the Filtered Items report
* @author Jean-François Morin (Université Laval)
*/
public class FilteredItemsQuery {
private List<String> collections = new ArrayList<>();
private List<QueryPredicate> queryPredicates = new ArrayList<>();
private long offset;
private int pageLimit;
private Set<Filter> filters = EnumSet.noneOf(Filter.class);
private List<String> additionalFields = new ArrayList<>();
/**
* Shortcut method that builds a FilteredItemsQuery instance
* from its building blocks.
* @param collectionUuids collection UUIDs to add
* @param predicates query predicates used to filter existing items
* @param pageLimit number of items per page
* @param filters filters to apply to existing items
* The filters mapping to true will be applied, others (either missing or
* mapping to false) will not.
* @param additionalFields additional fields to display in the resulting report
* @return a FilteredItemsQuery instance built from the provided parameters
*/
public static FilteredItemsQuery of(Collection<String> collectionUuids,
Collection<QueryPredicate> predicates, long offset, int pageLimit,
Collection<Filter> filters, Collection<String> additionalFields) {
var query = new FilteredItemsQuery();
Optional.ofNullable(collectionUuids).ifPresent(query.collections::addAll);
Optional.ofNullable(predicates).ifPresent(query.queryPredicates::addAll);
query.offset = offset;
query.pageLimit = pageLimit;
Optional.ofNullable(filters).ifPresent(query.filters::addAll);
Optional.ofNullable(additionalFields).ifPresent(query.additionalFields::addAll);
return query;
}
public List<String> getCollections() {
return collections;
}
public void setCollections(List<String> collections) {
this.collections.clear();
if (collections != null) {
this.collections.addAll(collections);
}
}
public List<QueryPredicate> getQueryPredicates() {
return queryPredicates;
}
public void setQueryPredicates(List<QueryPredicate> queryPredicates) {
this.queryPredicates.clear();
if (queryPredicates != null) {
this.queryPredicates.addAll(queryPredicates);
}
}
public long getOffset() {
return offset;
}
public void setOffset(long offset) {
this.offset = offset;
}
public int getPageLimit() {
return pageLimit;
}
public void setPageLimit(int pageLimit) {
this.pageLimit = pageLimit;
}
public Set<Filter> getFilters() {
return filters;
}
public void setFilters(Set<Filter> filters) {
this.filters.clear();
if (filters != null) {
this.filters.addAll(filters);
}
}
public List<String> getAdditionalFields() {
return additionalFields;
}
public void setAdditionalFields(List<String> additionalFields) {
this.additionalFields.clear();
if (additionalFields != null) {
this.additionalFields.addAll(additionalFields);
}
}
}

View File

@@ -0,0 +1,353 @@
/**
* 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.contentreport;
import static org.dspace.content.Item.ANY;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.content.Bitstream;
import org.dspace.content.Bundle;
import org.dspace.content.Item;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.services.factory.DSpaceServicesFactory;
/**
* Utility methods for applying some of the filters defined in the {@link Filter} enum.
*
* @author Jean-François Morin (Université Laval) (port to DSpace 7.x)
* @author Terry Brady, Georgetown University (original code in DSpace 6.x)
*/
public class ItemFilterUtil {
protected static ItemService itemService = ContentServiceFactory.getInstance().getItemService();
private static final Logger log = LogManager.getLogger(ItemFilterUtil.class);
public static final String[] MIMES_PDF = {"application/pdf"};
public static final String[] MIMES_JPG = {"image/jpeg"};
/**
* Supported bundle types.
* N.B.: Bundle names are used in metadata as they are named here.
* Do NOT change these names, the name() method is invoked at multiple
* locations in this class and enum Filter.
* If these names are to change, the name() invocations shall be changed
* so that they refer to these unchanged names, likely through a String property.
*/
enum BundleName {
ORIGINAL, TEXT, LICENSE, THUMBNAIL;
}
private ItemFilterUtil() {}
static String[] getDocumentMimeTypes() {
return DSpaceServicesFactory.getInstance().getConfigurationService()
.getArrayProperty("rest.report-mime-document");
}
static String[] getSupportedDocumentMimeTypes() {
return DSpaceServicesFactory.getInstance().getConfigurationService()
.getArrayProperty("rest.report-mime-document-supported");
}
static String[] getSupportedImageMimeTypes() {
return DSpaceServicesFactory.getInstance().getConfigurationService()
.getArrayProperty("rest.report-mime-document-image");
}
/**
* Counts the original bitstreams of a given item.
* @param item Provided item
* @return the number of original bitstreams in the item
*/
static int countOriginalBitstream(Item item) {
return countBitstream(BundleName.ORIGINAL, item);
}
/**
* Counts the bitstreams of a given item for a specific type.
* @param bundleName Type of bundle to filter bitstreams
* @param item Provided item
* @return the number of matching bitstreams in the item
*/
static int countBitstream(BundleName bundleName, Item item) {
return item.getBundles().stream()
.filter(bundle -> bundle.getName().equals(bundleName.name()))
.mapToInt(bundle -> bundle.getBitstreams().size())
.sum();
}
/**
* Retrieves the bitstream names of an given item for a specific bundle type.
* @param bundleName Type of bundle to filter bitstreams
* @param item Provided item
* @return the names of matching bitstreams in the item
*/
static List<String> getBitstreamNames(BundleName bundleName, Item item) {
return item.getBundles().stream()
.filter(bundle -> bundle.getName().equals(bundleName.name()))
.map(Bundle::getBitstreams)
.flatMap(List::stream)
.map(Bitstream::getName)
.collect(Collectors.toList());
}
/**
* Counts the original bitstreams of a given item matching one of a list of specific MIME types.
* @param context DSpace context
* @param item Provided item
* @param mimeList List of MIME types to filter bitstreams
* @return number of matching original bitstreams
*/
static int countOriginalBitstreamMime(Context context, Item item, String[] mimeList) {
return countBitstreamMime(context, BundleName.ORIGINAL, item, mimeList);
}
/**
* Counts the bitstreams of a given item for a specific type matching one of a list of specific MIME types.
* @param context DSpace context
* @param bundleName Type of bundle to filter bitstreams
* @param item Provided item
* @param mimeList List of MIME types to filter bitstreams
* @return number of matching bitstreams
*/
static int countBitstreamMime(Context context, BundleName bundleName, Item item, String[] mimeList) {
return item.getBundles().stream()
.filter(bundle -> bundle.getName().equals(bundleName.name()))
.map(Bundle::getBitstreams)
.flatMap(List::stream)
.mapToInt(bit -> {
int count = 0;
for (String mime : mimeList) {
try {
if (bit.getFormat(context).getMIMEType().equals(mime.trim())) {
count++;
}
} catch (SQLException e) {
log.error("Get format error for bitstream " + bit.getName());
}
}
return count;
})
.sum();
}
/**
* Counts the bitstreams of a given item for a specific type matching one of a list of specific descriptions.
* @param bundleName Type of bundle to filter bitstreams
* @param item Provided item
* @param descList List of descriptions to filter bitstreams
* @return number of matching bitstreams
*/
static int countBitstreamByDesc(BundleName bundleName, Item item, String[] descList) {
return item.getBundles().stream()
.filter(bundle -> bundle.getName().equals(bundleName.name()))
.map(Bundle::getBitstreams)
.flatMap(List::stream)
.filter(bit -> bit.getDescription() != null)
.mapToInt(bit -> {
int count = 0;
for (String desc : descList) {
String bitDesc = bit.getDescription();
if (bitDesc.equals(desc.trim())) {
count++;
}
}
return count;
})
.sum();
}
/**
* Counts the bitstreams of a given item smaller than a given size for a specific type
* matching one of a list of specific MIME types.
* @param context DSpace context
* @param bundleName Type of bundle to filter bitstreams
* @param item Provided item
* @param mimeList List of MIME types to filter bitstreams
* @param prop Configurable property providing the size to filter bitstreams
* @return number of matching bitstreams
*/
static int countBitstreamSmallerThanMinSize(
Context context, BundleName bundleName, Item item, String[] mimeList, String prop) {
long size = DSpaceServicesFactory.getInstance().getConfigurationService().getLongProperty(prop);
return item.getBundles().stream()
.filter(bundle -> bundle.getName().equals(bundleName.name()))
.map(Bundle::getBitstreams)
.flatMap(List::stream)
.mapToInt(bit -> {
int count = 0;
for (String mime : mimeList) {
try {
if (bit.getFormat(context).getMIMEType().equals(mime.trim())) {
if (bit.getSizeBytes() < size) {
count++;
}
}
} catch (SQLException e) {
log.error(e.getMessage(), e);
}
}
return count;
})
.sum();
}
/**
* Counts the bitstreams of a given item larger than a given size for a specific type
* matching one of a list of specific MIME types.
* @param context DSpace context
* @param bundleName Type of bundle to filter bitstreams
* @param item Provided item
* @param mimeList List of MIME types to filter bitstreams
* @param prop Configurable property providing the size to filter bitstreams
* @return number of matching bitstreams
*/
static int countBitstreamLargerThanMaxSize(
Context context, BundleName bundleName, Item item, String[] mimeList, String prop) {
long size = DSpaceServicesFactory.getInstance().getConfigurationService().getLongProperty(prop);
return item.getBundles().stream()
.filter(bundle -> bundle.getName().equals(bundleName.name()))
.map(Bundle::getBitstreams)
.flatMap(List::stream)
.mapToInt(bit -> {
int count = 0;
for (String mime : mimeList) {
try {
if (bit.getFormat(context).getMIMEType().equals(mime.trim())) {
if (bit.getSizeBytes() > size) {
count++;
}
}
} catch (SQLException e) {
log.error(e.getMessage(), e);
}
}
return count;
})
.sum();
}
/**
* Counts the original bitstreams of a given item whose MIME type starts with a specific prefix.
* @param context DSpace context
* @param item Provided item
* @param prefix Prefix to filter bitstreams
* @return number of matching original bitstreams
*/
static int countOriginalBitstreamMimeStartsWith(Context context, Item item, String prefix) {
return countBitstreamMimeStartsWith(context, BundleName.ORIGINAL, item, prefix);
}
/**
* Counts the bitstreams of a given item for a specific type whose MIME type starts with a specific prefix.
* @param context DSpace context
* @param bundleName Type of bundle to filter bitstreams
* @param item Provided item
* @param prefix Prefix to filter bitstreams
* @return number of matching bitstreams
*/
static int countBitstreamMimeStartsWith(Context context, BundleName bundleName, Item item, String prefix) {
return item.getBundles().stream()
.filter(bundle -> bundle.getName().equals(bundleName.name()))
.map(Bundle::getBitstreams)
.flatMap(List::stream)
.mapToInt(bit -> {
int count = 0;
try {
if (bit.getFormat(context).getMIMEType().startsWith(prefix)) {
count++;
}
} catch (SQLException e) {
log.error(e.getMessage(), e);
}
return count;
})
.sum();
}
/**
* Returns true if a given item has a bundle not matching a specific list of bundles.
* @param item Provided item
* @param bundleList List of bundle names to filter bundles
* @return true if the item has a (non-)matching bundle
*/
static boolean hasUnsupportedBundle(Item item, String[] bundleList) {
if (bundleList == null) {
return false;
}
Set<String> bundles = Arrays.stream(bundleList)
.collect(Collectors.toSet());
return item.getBundles().stream()
.anyMatch(bundle -> !bundles.contains(bundle.getName()));
}
static boolean hasOriginalBitstreamMime(Context context, Item item, String[] mimeList) {
return hasBitstreamMime(context, BundleName.ORIGINAL, item, mimeList);
}
static boolean hasBitstreamMime(Context context, BundleName bundleName, Item item, String[] mimeList) {
return countBitstreamMime(context, bundleName, item, mimeList) > 0;
}
/**
* Returns true if a given item has at least one field of a specific list whose value
* matches a provided regular expression.
* @param item Provided item
* @param fieldList List of fields to check
* @param regex Regular expression to check field values against
* @return true if there is at least one matching field, false otherwise
*/
static boolean hasMetadataMatch(Item item, String fieldList, Pattern regex) {
if ("*".equals(fieldList)) {
return itemService.getMetadata(item, ANY, ANY, ANY, ANY).stream()
.anyMatch(md -> regex.matcher(md.getValue()).matches());
}
return Arrays.stream(fieldList.split(","))
.map(field -> itemService.getMetadataByMetadataString(item, field.trim()))
.flatMap(List::stream)
.anyMatch(md -> regex.matcher(md.getValue()).matches());
}
/**
* Returns true if a given item has at all fields of a specific list whose values
* match a provided regular expression.
* @param item Provided item
* @param fieldList List of fields to check
* @param regex Regular expression to check field values against
* @return true if all specified fields match, false otherwise
*/
static boolean hasOnlyMetadataMatch(Item item, String fieldList, Pattern regex) {
if ("*".equals(fieldList)) {
return itemService.getMetadata(item, ANY, ANY, ANY, ANY).stream()
.allMatch(md -> regex.matcher(md.getValue()).matches());
}
return Arrays.stream(fieldList.split(","))
.map(field -> itemService.getMetadataByMetadataString(item, field.trim()))
.flatMap(List::stream)
.allMatch(md -> regex.matcher(md.getValue()).matches());
}
static boolean recentlyModified(Item item, int days) {
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DATE, -days);
return cal.getTime().before(item.getLastModified());
}
}

View File

@@ -0,0 +1,131 @@
/**
* 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.contentreport;
import java.util.Arrays;
import java.util.function.BiFunction;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.lang3.function.TriFunction;
import org.dspace.content.MetadataValue;
import org.dspace.content.MetadataValue_;
import org.dspace.util.DSpacePostgreSQLDialect;
import org.dspace.util.JpaCriteriaBuilderKit;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Property;
import org.hibernate.criterion.Restrictions;
import org.hibernate.type.StandardBasicTypes;
/**
* Operators available for creating predicates to query the
* Filtered Items report
* @author Jean-François Morin (Université Laval)
*/
public enum QueryOperator {
EXISTS("exists", true, false,
(val, regexClause) -> Property.forName("mv.value").isNotNull(),
(val, regexClause, jpaKit) -> jpaKit.criteriaBuilder().isNotNull(jpaKit.root().get(MetadataValue_.VALUE))),
DOES_NOT_EXIST("doesnt_exist", true, true,
(val, regexClause) -> EXISTS.buildPredicate(val, regexClause),
(val, regexClause, jpaKit) -> EXISTS.buildJpaPredicate(val, regexClause, jpaKit)),
EQUALS("equals", true, false,
(val, regexClause) -> Property.forName("mv.value").eq(val),
(val, regexClause, jpaKit) -> jpaKit.criteriaBuilder().equal(jpaKit.root().get(MetadataValue_.VALUE), val)),
DOES_NOT_EQUAL("not_equals", true, true,
(val, regexClause) -> EQUALS.buildPredicate(val, regexClause),
(val, regexClause, jpaKit) -> EQUALS.buildJpaPredicate(val, regexClause, jpaKit)),
LIKE("like", true, false,
(val, regexClause) -> Property.forName("mv.value").like(val),
(val, regexClause, jpaKit) -> jpaKit.criteriaBuilder().like(jpaKit.root().get(MetadataValue_.VALUE), val)),
NOT_LIKE("not_like", true, true,
(val, regexClause) -> LIKE.buildPredicate(val, regexClause),
(val, regexClause, jpaKit) -> LIKE.buildJpaPredicate(val, regexClause, jpaKit)),
CONTAINS("contains", true, false,
(val, regexClause) -> Property.forName("mv.value").like("%" + val + "%"),
(val, regexClause, jpaKit) -> LIKE.buildJpaPredicate("%" + val + "%", regexClause, jpaKit)),
DOES_NOT_CONTAIN("doesnt_contain", true, true,
(val, regexClause) -> CONTAINS.buildPredicate(val, regexClause),
(val, regexClause, jpaKit) -> CONTAINS.buildJpaPredicate(val, regexClause, jpaKit)),
MATCHES("matches", false, false,
(val, regexClause) -> Restrictions.sqlRestriction(regexClause, val, StandardBasicTypes.STRING),
(val, regexClause, jpaKit) -> regexPredicate(val, DSpacePostgreSQLDialect.REGEX_MATCHES, jpaKit)),
DOES_NOT_MATCH("doesnt_match", false, false,
(val, regexClause) -> Restrictions.not(Restrictions.sqlRestriction(
regexClause, val, StandardBasicTypes.STRING)),
(val, regexClause, jpaKit) -> regexPredicate(val, DSpacePostgreSQLDialect.REGEX_NOT_MATCHES, jpaKit));
private final String code;
/** Criteria builder for the old Hibernate API */
@Deprecated(forRemoval = true)
private final BiFunction<String, String, Criterion> criterionBuilder;
private final TriFunction<String, String, JpaCriteriaBuilderKit<MetadataValue>, Predicate> predicateBuilder;
private final boolean usesRegex;
private final boolean negate;
QueryOperator(String code, boolean usesRegex, boolean negate,
BiFunction<String, String, Criterion> criterionBuilder,
TriFunction<String, String, JpaCriteriaBuilderKit<MetadataValue>, Predicate> predicateBuilder) {
this.code = code;
this.usesRegex = usesRegex;
this.negate = negate;
this.criterionBuilder = criterionBuilder;
this.predicateBuilder = predicateBuilder;
}
@JsonProperty
public String getCode() {
return code;
}
public boolean getUsesRegex() {
return usesRegex;
}
public boolean getNegate() {
return negate;
}
public Criterion buildPredicate(String val, String regexClause) {
return criterionBuilder.apply(val, regexClause);
}
public Predicate buildJpaPredicate(String val, String regexClause, JpaCriteriaBuilderKit<MetadataValue> jpaKit) {
return predicateBuilder.apply(val, regexClause, jpaKit);
}
@JsonCreator
public static QueryOperator get(String code) {
return Arrays.stream(values())
.filter(item -> item.code.equalsIgnoreCase(code))
.findFirst()
.orElse(null);
}
public BiFunction<String, String, Criterion> getCriterionBuilder() {
return criterionBuilder;
}
private static Predicate regexPredicate(String val, String regexFunction,
JpaCriteriaBuilderKit<MetadataValue> jpaKit) {
// Source: https://stackoverflow.com/questions/24995881/use-regular-expressions-in-jpa-criteriabuilder
CriteriaBuilder builder = jpaKit.criteriaBuilder();
Expression<String> patternExpression = builder.<String>literal(val);
Path<String> path = jpaKit.root().get(MetadataValue_.VALUE);
// "matches" comes from the name of the regex function
// defined in class DSpacePostgreSQLDialect
return builder.equal(builder
.function(regexFunction, Boolean.class, path, patternExpression), Boolean.TRUE);
}
}

View File

@@ -0,0 +1,69 @@
/**
* 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.contentreport;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.dspace.content.MetadataField;
/**
* Data structure representing a query predicate used by the Filtered Items report
* to filter items to retrieve.
* @author Jean-François Morin (Université Laval)
*/
public class QueryPredicate {
private List<MetadataField> fields = new ArrayList<>();
private QueryOperator operator;
private String value;
/**
* Shortcut method that builds a QueryPredicate from a single field, an operator, and a value.
* @param field Predicate subject
* @param operator Predicate operator
* @param value Predicate object
* @return a QueryPredicate instance built from the provided parameters
*/
public static QueryPredicate of(MetadataField field, QueryOperator operator, String value) {
var predicate = new QueryPredicate();
predicate.fields.add(field);
predicate.operator = operator;
predicate.value = value;
return predicate;
}
/**
* Shortcut method that builds a QueryPredicate from a list of fields, an operator, and a value.
* @param fields Fields that form the predicate subject
* @param operator Predicate operator
* @param value Predicate object
* @return a QueryPredicate instance built from the provided parameters
*/
public static QueryPredicate of(Collection<MetadataField> fields, QueryOperator operator, String value) {
var predicate = new QueryPredicate();
predicate.fields.addAll(fields);
predicate.operator = operator;
predicate.value = value;
return predicate;
}
public List<MetadataField> getFields() {
return fields;
}
public QueryOperator getOperator() {
return operator;
}
public String getValue() {
return value;
}
}

View File

@@ -0,0 +1,55 @@
/**
* 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.contentreport.service;
import java.sql.SQLException;
import java.util.Collection;
import java.util.List;
import org.dspace.content.MetadataField;
import org.dspace.contentreport.Filter;
import org.dspace.contentreport.FilteredCollection;
import org.dspace.contentreport.FilteredItems;
import org.dspace.contentreport.FilteredItemsQuery;
import org.dspace.core.Context;
public interface ContentReportService {
/**
* Returns <code>true<</code> if Content Reports are enabled.
* @return <code>true<</code> if Content Reports are enabled
*/
boolean getEnabled();
/**
* Retrieves item statistics per collection according to a set of Boolean filters.
* @param context DSpace context
* @param filters Set of filters
* @return a list of collections with the requested statistics for each of them
*/
List<FilteredCollection> findFilteredCollections(Context context, Collection<Filter> filters);
/**
* Retrieves a list of items according to a set of criteria.
* @param context DSpace context
* @param query structured query to find items against
* @return a list of items filtered according to the provided query
*/
FilteredItems findFilteredItems(Context context, FilteredItemsQuery query);
/**
* Converts a metadata field name to a list of {@link MetadataField} instances
* (one if no wildcards are used, possibly more otherwise).
* @param context DSpace context
* @param metadataField field to search for
* @return a corresponding list of {@link MetadataField} entries
*/
List<MetadataField> getMetadataFields(org.dspace.core.Context context, String metadataField)
throws SQLException;
}

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.correctiontype;
import java.sql.SQLException;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Item;
import org.dspace.content.QAEvent;
import org.dspace.core.Context;
import org.dspace.qaevent.service.dto.QAMessageDTO;
/**
* Interface class that model the CorrectionType.
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com)
*/
public interface CorrectionType {
/**
* Retrieves the unique identifier associated to the CorrectionType.
*/
public String getId();
/**
* Retrieves the topic associated with the to the CorrectionType.
*/
public String getTopic();
/**
* Checks whether the CorrectionType required related item.
*/
public boolean isRequiredRelatedItem();
/**
* Checks whether target item is allowed for current CorrectionType
*
* @param context Current DSpace session
* @param targetItem Target item
* @throws AuthorizeException if authorize error
* @throws SQLException if there's a database problem
*/
public boolean isAllowed(Context context, Item targetItem) throws AuthorizeException, SQLException;
/**
* Checks whether target item and related item are allowed for current CorrectionType
*
* @param context Current DSpace session
* @param targetItem Target item
* @param relatedItem Related item
* @throws AuthorizeException if authorize error
* @throws SQLException if there's a database problem
*/
public boolean isAllowed(Context context, Item targetItem, Item relatedItem) throws AuthorizeException,SQLException;
/**
* Creates a QAEvent for a specific target item.
*
* @param context Current DSpace session
* @param targetItem Target item
* @param reason Reason
* @return QAEvent
*/
public QAEvent createCorrection(Context context, Item targetItem, QAMessageDTO reason);
/**
* Creates a QAEvent for a target item and related item.
* @param context Current DSpace session
* @param targetItem Target item
* @param relatedItem Related item
* @param reason Reason
* @return QAEvent
*/
public QAEvent createCorrection(Context context, Item targetItem, Item relatedItem, QAMessageDTO reason);
}

View File

@@ -0,0 +1,141 @@
/**
* 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.correctiontype;
import static org.dspace.content.QAEvent.DSPACE_USERS_SOURCE;
import static org.dspace.correctiontype.WithdrawnCorrectionType.WITHDRAWAL_REINSTATE_GROUP;
import java.sql.SQLException;
import java.util.Date;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.lang3.StringUtils;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.Item;
import org.dspace.content.QAEvent;
import org.dspace.core.Context;
import org.dspace.eperson.Group;
import org.dspace.eperson.service.GroupService;
import org.dspace.qaevent.service.QAEventService;
import org.dspace.qaevent.service.dto.CorrectionTypeMessageDTO;
import org.dspace.qaevent.service.dto.QAMessageDTO;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Implementation class for {@link CorrectionType}
* that will reinstate target item if it's withdrawn.
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com)
*/
public class ReinstateCorrectionType implements CorrectionType, InitializingBean {
private String id;
private String topic;
private String creationForm;
@Autowired
private GroupService groupService;
@Autowired
private QAEventService qaEventService;
@Autowired
private AuthorizeService authorizeService;
@Autowired
private ConfigurationService configurationService;
@Override
public boolean isAllowed(Context context, Item targetItem) throws SQLException {
if (!targetItem.isWithdrawn()) {
return false;
}
boolean isAdmin = authorizeService.isAdmin(context);
if (!currentUserIsMemberOfwithdrawalReinstateGroup(context) && !isAdmin) {
return false;
}
long tot = qaEventService.countSourcesByTarget(context, targetItem.getID());
return tot == 0;
}
private boolean currentUserIsMemberOfwithdrawalReinstateGroup(Context context) throws SQLException {
String groupName = configurationService.getProperty(WITHDRAWAL_REINSTATE_GROUP);
if (StringUtils.isBlank(groupName)) {
return false;
}
Group withdrawalReinstateGroup = groupService.findByName(context, groupName);
return withdrawalReinstateGroup != null && groupService.isMember(context, withdrawalReinstateGroup);
}
@Override
public boolean isAllowed(Context context, Item targetItem, Item relatedItem) throws AuthorizeException,
SQLException {
return isAllowed(context, targetItem);
}
@Override
public QAEvent createCorrection(Context context, Item targetItem, QAMessageDTO reason) {
ObjectNode reasonJson = createReasonJson(reason);
QAEvent qaEvent = new QAEvent(DSPACE_USERS_SOURCE,
context.getCurrentUser().getID().toString(),
targetItem.getID().toString(),
targetItem.getName(),
this.getTopic(),
1.0,
reasonJson.toString(),
new Date()
);
qaEventService.store(context, qaEvent);
return qaEvent;
}
private ObjectNode createReasonJson(QAMessageDTO reason) {
CorrectionTypeMessageDTO mesasge = (CorrectionTypeMessageDTO) reason;
ObjectNode jsonNode = new ObjectMapper().createObjectNode();
jsonNode.put("reason", mesasge.getReason());
return jsonNode;
}
@Override
public QAEvent createCorrection(Context context, Item targetItem, Item relatedItem, QAMessageDTO reason) {
return this.createCorrection(context, targetItem, reason);
}
@Override
public boolean isRequiredRelatedItem() {
return false;
}
@Override
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@Override
public String getTopic() {
return topic;
}
public void setTopic(String topic) {
this.topic = topic;
}
@Override
public void afterPropertiesSet() throws Exception {}
public void setCreationForm(String creationForm) {
this.creationForm = creationForm;
}
}

View File

@@ -0,0 +1,149 @@
/**
* 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.correctiontype;
import static org.dspace.content.QAEvent.DSPACE_USERS_SOURCE;
import static org.dspace.core.Constants.READ;
import java.sql.SQLException;
import java.util.Date;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.lang3.StringUtils;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.Item;
import org.dspace.content.QAEvent;
import org.dspace.core.Context;
import org.dspace.eperson.Group;
import org.dspace.eperson.service.GroupService;
import org.dspace.qaevent.service.QAEventService;
import org.dspace.qaevent.service.dto.CorrectionTypeMessageDTO;
import org.dspace.qaevent.service.dto.QAMessageDTO;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Implementation class for {@link CorrectionType}
* that will withdrawn target item if it archived and wasn't withdrawn alredy.
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com)
*/
public class WithdrawnCorrectionType implements CorrectionType, InitializingBean {
public static final String WITHDRAWAL_REINSTATE_GROUP = "qaevents.withdraw-reinstate.group";
private String id;
private String topic;
private String creationForm;
@Autowired
private GroupService groupService;
@Autowired
private QAEventService qaEventService;
@Autowired
private AuthorizeService authorizeService;
@Autowired
private ConfigurationService configurationService;
@Override
public boolean isAllowed(Context context, Item targetItem) throws SQLException {
if (targetItem.isWithdrawn() || !targetItem.isArchived()) {
return false;
}
try {
authorizeService.authorizeAction(context, targetItem, READ);
} catch (AuthorizeException e) {
return false;
}
boolean isAdmin = authorizeService.isAdmin(context);
if (!currentUserIsMemberOfwithdrawalReinstateGroup(context) && !isAdmin) {
return false;
}
long tot = qaEventService.countSourcesByTarget(context, targetItem.getID());
return tot == 0;
}
private boolean currentUserIsMemberOfwithdrawalReinstateGroup(Context context) throws SQLException {
String groupName = configurationService.getProperty(WITHDRAWAL_REINSTATE_GROUP);
if (StringUtils.isBlank(groupName)) {
return false;
}
Group withdrawalReinstateGroup = groupService.findByName(context, groupName);
return withdrawalReinstateGroup != null && groupService.isMember(context, withdrawalReinstateGroup);
}
@Override
public QAEvent createCorrection(Context context, Item targetItem, QAMessageDTO reason) {
ObjectNode reasonJson = createReasonJson(reason);
QAEvent qaEvent = new QAEvent(DSPACE_USERS_SOURCE,
context.getCurrentUser().getID().toString(),
targetItem.getID().toString(),
targetItem.getName(),
this.getTopic(),
1.0,
reasonJson.toString(),
new Date()
);
qaEventService.store(context, qaEvent);
return qaEvent;
}
private ObjectNode createReasonJson(QAMessageDTO reason) {
CorrectionTypeMessageDTO mesasge = (CorrectionTypeMessageDTO) reason;
ObjectNode jsonNode = new ObjectMapper().createObjectNode();
jsonNode.put("reason", mesasge.getReason());
return jsonNode;
}
@Override
public boolean isAllowed(Context context, Item targetItem, Item relatedItem)
throws AuthorizeException, SQLException {
return isAllowed(context, targetItem);
}
@Override
public QAEvent createCorrection(Context context, Item targetItem, Item relatedItem, QAMessageDTO reason) {
return createCorrection(context, targetItem, reason);
}
@Override
public boolean isRequiredRelatedItem() {
return false;
}
@Override
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@Override
public String getTopic() {
return topic;
}
public void setTopic(String topic) {
this.topic = topic;
}
@Override
public void afterPropertiesSet() throws Exception {}
public void setCreationForm(String creationForm) {
this.creationForm = creationForm;
}
}

View File

@@ -0,0 +1,58 @@
/**
* 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.correctiontype.service;
import java.sql.SQLException;
import java.util.List;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.dspace.correctiontype.CorrectionType;
/**
* Service interface class for the CorrectionType object.
*
* @author Mohamed Eskander (mohamed.eskander at 4science.com)
*/
public interface CorrectionTypeService {
/**
* Retrieves a CorrectionType object from the system based on a unique identifier.
*
* @param id The unique identifier of the CorrectionType object to be retrieved.
* @return The CorrectionType object corresponding to the provided identifier,
* or null if no object is found.
*/
public CorrectionType findOne(String id);
/**
* Retrieves a list of all CorrectionType objects available in the system.
*
* @return Returns a List containing all CorrectionType objects in the system.
*/
public List<CorrectionType> findAll();
/**
* Retrieves a list of CorrectionType objects related to the provided Item.
*
* @param context Current DSpace session.
* @param item Target item
* @throws AuthorizeException If authorize error
* @throws SQLException If a database error occurs during the operation.
*/
public List<CorrectionType> findByItem(Context context, Item item) throws AuthorizeException, SQLException;
/**
* Retrieves a CorrectionType object associated with a specific topic.
*
* @param topic The topic for which the CorrectionType object is to be retrieved.
*/
public CorrectionType findByTopic(String topic);
}

View File

@@ -0,0 +1,64 @@
/**
* 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.correctiontype.service.impl;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.collections4.CollectionUtils;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.dspace.correctiontype.CorrectionType;
import org.dspace.correctiontype.service.CorrectionTypeService;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Service implementation class for the CorrectionType object.
*
* @author Mohamed Eskander (mohamed.eskander at 4science.com)
*/
public class CorrectionTypeServiceImpl implements CorrectionTypeService {
@Autowired
private List<CorrectionType> correctionTypes;
@Override
public CorrectionType findOne(String id) {
return findAll().stream()
.filter(correctionType -> correctionType.getId().equals(id))
.findFirst()
.orElse(null);
}
@Override
public List<CorrectionType> findAll() {
return CollectionUtils.isNotEmpty(correctionTypes) ? correctionTypes : List.of();
}
@Override
public List<CorrectionType> findByItem(Context context, Item item) throws AuthorizeException, SQLException {
List<CorrectionType> correctionTypes = new ArrayList<>();
for (CorrectionType correctionType : findAll()) {
if (correctionType.isAllowed(context, item)) {
correctionTypes.add(correctionType);
}
}
return correctionTypes;
}
@Override
public CorrectionType findByTopic(String topic) {
return findAll().stream()
.filter(correctionType -> correctionType.getTopic().equals(topic))
.findFirst()
.orElse(null);
}
}

View File

@@ -17,6 +17,7 @@ import org.dspace.app.util.DCInput;
import org.dspace.app.util.DCInputSet;
import org.dspace.app.util.DCInputsReader;
import org.dspace.app.util.DCInputsReaderException;
import org.dspace.content.Collection;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
@@ -69,7 +70,7 @@ public class RequiredMetadata extends AbstractCurationTask {
handle = "in workflow";
}
sb.append("Item: ").append(handle);
for (String req : getReqList(item.getOwningCollection().getHandle())) {
for (String req : getReqList(item.getOwningCollection())) {
List<MetadataValue> vals = itemService.getMetadataByMetadataString(item, req);
if (vals.size() == 0) {
sb.append(" missing required field: ").append(req);
@@ -91,14 +92,14 @@ public class RequiredMetadata extends AbstractCurationTask {
}
}
protected List<String> getReqList(String handle) throws DCInputsReaderException {
List<String> reqList = reqMap.get(handle);
protected List<String> getReqList(Collection collection) throws DCInputsReaderException {
List<String> reqList = reqMap.get(collection.getHandle());
if (reqList == null) {
reqList = reqMap.get("default");
}
if (reqList == null) {
reqList = new ArrayList<String>();
List<DCInputSet> inputSet = reader.getInputsByCollectionHandle(handle);
List<DCInputSet> inputSet = reader.getInputsByCollection(collection);
for (DCInputSet inputs : inputSet) {
for (DCInput[] row : inputs.getFields()) {
for (DCInput input : row) {

View File

@@ -7,14 +7,20 @@
*/
package org.dspace.discovery;
import static org.dspace.discovery.IndexClientOptions.TYPE_OPTION;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.ParseException;
import org.apache.commons.lang3.StringUtils;
import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.DSpaceObject;
@@ -51,6 +57,17 @@ public class IndexClient extends DSpaceRunnable<IndexDiscoveryScriptConfiguratio
return;
}
String type = null;
if (commandLine.hasOption(TYPE_OPTION)) {
List<String> indexableObjectTypes = IndexObjectFactoryFactory.getInstance().getIndexFactories().stream()
.map((indexFactory -> indexFactory.getType())).collect(Collectors.toList());
type = commandLine.getOptionValue(TYPE_OPTION);
if (!indexableObjectTypes.contains(type)) {
handler.handleException(String.format("%s is not a valid indexable object type, options: %s",
type, Arrays.toString(indexableObjectTypes.toArray())));
}
}
/** Acquire from dspace-services in future */
/**
* new DSpace.getServiceManager().getServiceByName("org.dspace.discovery.SolrIndexer");
@@ -113,6 +130,10 @@ public class IndexClient extends DSpaceRunnable<IndexDiscoveryScriptConfiguratio
} else if (indexClientOptions == IndexClientOptions.BUILD ||
indexClientOptions == IndexClientOptions.BUILDANDSPELLCHECK) {
handler.logInfo("(Re)building index from scratch.");
if (StringUtils.isNotBlank(type)) {
handler.logWarning(String.format("Type option, %s, not applicable for entire index rebuild option, b" +
", type will be ignored", TYPE_OPTION));
}
indexer.deleteIndex();
indexer.createIndex(context);
if (indexClientOptions == IndexClientOptions.BUILDANDSPELLCHECK) {
@@ -133,14 +154,14 @@ public class IndexClient extends DSpaceRunnable<IndexDiscoveryScriptConfiguratio
} else if (indexClientOptions == IndexClientOptions.UPDATE ||
indexClientOptions == IndexClientOptions.UPDATEANDSPELLCHECK) {
handler.logInfo("Updating Index");
indexer.updateIndex(context, false);
indexer.updateIndex(context, false, type);
if (indexClientOptions == IndexClientOptions.UPDATEANDSPELLCHECK) {
checkRebuildSpellCheck(commandLine, indexer);
}
} else if (indexClientOptions == IndexClientOptions.FORCEUPDATE ||
indexClientOptions == IndexClientOptions.FORCEUPDATEANDSPELLCHECK) {
handler.logInfo("Updating Index");
indexer.updateIndex(context, true);
indexer.updateIndex(context, true, type);
if (indexClientOptions == IndexClientOptions.FORCEUPDATEANDSPELLCHECK) {
checkRebuildSpellCheck(commandLine, indexer);
}

Some files were not shown because too many files have changed in this diff Show More