diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 8b3cb1d017a1..679543cb01aa 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -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"
+ java: 17
+ 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
@@ -34,7 +34,7 @@ jobs:
# - xml.skip => Skip all XML/XSLT validation by xml-maven-plugin
# - failsafe.rerunFailingTestsCount => try again for flakey tests, and keep track of/report on number of retries
- type: "Integration Tests"
- java: 11
+ java: 17
mvnflags: "-DskipIntegrationTests=false -Denforcer.skip=true -Dcheckstyle.skip=true -Dlicense.skip=true -Dxml.skip=true -Dfailsafe.rerunFailingTestsCount=2"
resultsdir: "**/target/failsafe-reports/**"
# Do NOT exit immediately if one matrix job fails
@@ -78,35 +78,37 @@ jobs:
path: 'dspace/target/site/jacoco-aggregate/jacoco.xml'
retention-days: 14
- # Codecov upload is a separate job in order to allow us to restart this separate from the entire build/test
- # job above. This is necessary because Codecov uploads seem to randomly fail at times.
- # See https://community.codecov.com/t/upload-issues-unable-to-locate-build-via-github-actions-api/3954
- codecov:
- # Must run after 'tests' job above
- needs: tests
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
+ # UMD Customization
+ # # Codecov upload is a separate job in order to allow us to restart this separate from the entire build/test
+ # # job above. This is necessary because Codecov uploads seem to randomly fail at times.
+ # # See https://community.codecov.com/t/upload-issues-unable-to-locate-build-via-github-actions-api/3954
+ # codecov:
+ # # Must run after 'tests' job above
+ # needs: tests
+ # runs-on: ubuntu-latest
+ # steps:
+ # - name: Checkout
+ # uses: actions/checkout@v4
- # Download artifacts from previous 'tests' job
- - name: Download coverage artifacts
- uses: actions/download-artifact@v4
+ # # Download artifacts from previous 'tests' job
+ # - name: Download coverage artifacts
+ # uses: actions/download-artifact@v4
- # Now attempt upload to Codecov using its action.
- # NOTE: We use a retry action to retry the Codecov upload if it fails the first time.
- #
- # Retry action: https://github.com/marketplace/actions/retry-action
- # Codecov action: https://github.com/codecov/codecov-action
- - name: Upload coverage to Codecov.io
- uses: Wandalen/wretry.action@v1.3.0
- with:
- action: codecov/codecov-action@v4
- # Ensure codecov-action throws an error when it fails to upload
- with: |
- fail_ci_if_error: true
- token: ${{ secrets.CODECOV_TOKEN }}
- # Try re-running action 5 times max
- attempt_limit: 5
- # Run again in 30 seconds
- attempt_delay: 30000
+ # # Now attempt upload to Codecov using its action.
+ # # NOTE: We use a retry action to retry the Codecov upload if it fails the first time.
+ # #
+ # # Retry action: https://github.com/marketplace/actions/retry-action
+ # # Codecov action: https://github.com/codecov/codecov-action
+ # - name: Upload coverage to Codecov.io
+ # uses: Wandalen/wretry.action@v1.3.0
+ # with:
+ # action: codecov/codecov-action@v4
+ # # Ensure codecov-action throws an error when it fails to upload
+ # with: |
+ # fail_ci_if_error: true
+ # token: ${{ secrets.CODECOV_TOKEN }}
+ # # Try re-running action 5 times max
+ # attempt_limit: 5
+ # # Run again in 30 seconds
+ # attempt_delay: 30000
+ # End UMD Customization
diff --git a/.github/workflows/codescan.yml b/.github/workflows/codescan.yml
index 1e3d835e2713..3a563c6fa39c 100644
--- a/.github/workflows/codescan.yml
+++ b/.github/workflows/codescan.yml
@@ -41,7 +41,7 @@ jobs:
- name: Install JDK
uses: actions/setup-java@v4
with:
- java-version: 11
+ java-version: 17
distribution: 'temurin'
# Initializes the CodeQL tools for scanning.
diff --git a/.github/workflows/reusable-docker-build.yml b/.github/workflows/reusable-docker-build.yml
index 12aa0cfe2864..019dab51eddb 100644
--- a/.github/workflows/reusable-docker-build.yml
+++ b/.github/workflows/reusable-docker-build.yml
@@ -68,9 +68,9 @@ env:
# See "Redeploy" steps below for more details.
REDEPLOY_SANDBOX_URL: ${{ secrets.REDEPLOY_SANDBOX_URL }}
REDEPLOY_DEMO_URL: ${{ secrets.REDEPLOY_DEMO_URL }}
- # Current DSpace maintenance branch (and architecture) which is deployed to demo.dspace.org / sandbox.dspace.org
- # (NOTE: No deployment branch specified for sandbox.dspace.org as it uses the default_branch)
- DEPLOY_DEMO_BRANCH: 'dspace-8_x'
+ # Current DSpace branches (and architecture) which are deployed to demo.dspace.org & sandbox.dspace.org respectively
+ DEPLOY_DEMO_BRANCH: 'dspace-7_x'
+ DEPLOY_SANDBOX_BRANCH: 'main'
DEPLOY_ARCH: 'linux/amd64'
jobs:
@@ -174,7 +174,7 @@ jobs:
!matrix.isPR &&
env.REDEPLOY_SANDBOX_URL != '' &&
matrix.arch == env.DEPLOY_ARCH &&
- github.ref_name == github.event.repository.default_branch
+ github.ref_name == env.DEPLOY_SANDBOX_BRANCH
run: |
curl -X POST $REDEPLOY_SANDBOX_URL
diff --git a/.lgtm.yml b/.lgtm.yml
deleted file mode 100644
index 132de8a6de5a..000000000000
--- a/.lgtm.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-# LGTM Settings (https://lgtm.com/)
-# For reference, see https://lgtm.com/help/lgtm/lgtm.yml-configuration-file
-# or template at https://lgtm.com/static/downloads/lgtm.template.yml
-
-extraction:
- java:
- index:
- # Specify the Java version required to build the project
- java_version: 11
diff --git a/Dockerfile b/Dockerfile
index 103e8ebd97e7..01046f485f5b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,15 +1,16 @@
# This image will be published as dspace/dspace
# See https://github.com/DSpace/DSpace/tree/main/dspace/src/main/docker for usage details
#
-# - note: default tag for branch: dspace/dspace: dspace/dspace:dspace-7_x
+# - note: default tag for branch: dspace/dspace: dspace/dspace:latest
-# This Dockerfile uses JDK11 by default, but has also been tested with JDK17.
-# To build with JDK17, use "--build-arg JDK_VERSION=17"
-ARG JDK_VERSION=11
+# This Dockerfile uses JDK17 by default.
+# To build with other versions, use "--build-arg JDK_VERSION=[value]"
+ARG JDK_VERSION=17
+ARG DSPACE_VERSION=latest
# Step 1 - Run Maven Build
# UMD Customization
-FROM docker.lib.umd.edu/drum-dependencies-7_x:latest as build
+FROM docker.lib.umd.edu/drum-dependencies-8_x:${DSPACE_VERSION} as build
# End UMD Customization
ARG TARGET_DIR=dspace-installer
WORKDIR /app
@@ -20,7 +21,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.
@@ -28,6 +29,8 @@ ENV MAVEN_FLAGS="-P-test-environment -Denforcer.skip=true -Dcheckstyle.skip=true
RUN mvn --no-transfer-progress package ${MAVEN_FLAGS} && \
mv /app/dspace/target/${TARGET_DIR}/* /install && \
mvn clean
+# Remove the server webapp to keep image small.
+RUN rm -rf /install/webapps/server/
# Step 2 - Run Ant Deploy
FROM eclipse-temurin:${JDK_VERSION} as ant_build
@@ -50,25 +53,21 @@ RUN mkdir $ANT_HOME && \
# Run necessary 'ant' deploy scripts
RUN ant init_installation update_configs update_code update_webapps
-# Step 3 - Run tomcat
-# Create a new tomcat image that does not retain the the build directory contents
-FROM tomcat:9-jdk${JDK_VERSION}
+# Step 3 - Start up DSpace via Runnable JAR
+FROM eclipse-temurin:${JDK_VERSION}
# NOTE: DSPACE_INSTALL must align with the "dspace.dir" default configuration.
ENV DSPACE_INSTALL=/dspace
# Copy the /dspace directory from 'ant_build' container to /dspace in this container
COPY --from=ant_build /dspace $DSPACE_INSTALL
-# Expose Tomcat port and AJP port
-EXPOSE 8080 8009
+WORKDIR $DSPACE_INSTALL
+# Expose Tomcat port
+EXPOSE 8080
# Give java extra memory (2GB)
-# UMD Customization
-ENV DSPACE_INSTALL=/dspace \
- JAVA_OPTS=-Xmx2000m \
- TZ=America/New_York
-# End UMD Customization
+ENV JAVA_OPTS=-Xmx2000m
-# 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/)
# UMD Customization
+ENV TZ=America/New_York
+
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y \
rsync \
@@ -81,12 +80,7 @@ RUN apt-get update && \
vim \
python3-lxml && \
mkfifo /var/spool/postfix/public/pickup && \
- ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone && \
- rm -rf /usr/local/tomcat/webapps/* && \
- ln -s $DSPACE_INSTALL/webapps/server /usr/local/tomcat/webapps/server
+ ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# End UMD Customization
-# 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
+# On startup, run DSpace Runnable JAR
+ENTRYPOINT ["java", "-jar", "webapps/server-boot.jar", "--dspace.dir=$DSPACE_INSTALL"]
diff --git a/Dockerfile.ant b/Dockerfile.ant
index 6b4a021995c5..47a6f1a5fe67 100644
--- a/Dockerfile.ant
+++ b/Dockerfile.ant
@@ -1,9 +1,9 @@
# This Docker image is based on Step 2 in "Dockerfile", with some steps removed
# as they will be performed in the Dockerfiles that use this image
# (Dockerfile.dev, Dockerfile.dev-additions)
-ARG JDK_VERSION=11
+ARG JDK_VERSION=17
-FROM openjdk:${JDK_VERSION}-slim as ant_build
+FROM eclipse-temurin:${JDK_VERSION} as ant_build
# Create the initial install deployment using ANT
ENV ANT_VERSION 1.10.13
ENV ANT_HOME /tmp/ant-$ANT_VERSION
diff --git a/Dockerfile.ci b/Dockerfile.ci
index 00ad1c85590c..413a9e4296f8 100644
--- a/Dockerfile.ci
+++ b/Dockerfile.ci
@@ -5,7 +5,7 @@
# the application. It should _not_ be used for creating Docker images for use
# in production.
-FROM maven:3.8.6-openjdk-11-slim
+FROM maven:3.8.6-eclipse-temurin-17
# Install git, as it is needed by the Jenkinsfile
RUN apt-get update && \
diff --git a/Dockerfile.cli b/Dockerfile.cli
index 65f45bfb09b1..49d5a8f7e9c4 100644
--- a/Dockerfile.cli
+++ b/Dockerfile.cli
@@ -1,15 +1,16 @@
# This image will be published as dspace/dspace-cli
# See https://github.com/DSpace/DSpace/tree/main/dspace/src/main/docker for usage details
#
-# - note: default tag for branch: dspace/dspace-cli: dspace/dspace-cli:dspace-7_x
+# - note: default tag for branch: dspace/dspace-cli: dspace/dspace-cli:latest
-# This Dockerfile uses JDK11 by default, but has also been tested with JDK17.
-# To build with JDK17, use "--build-arg JDK_VERSION=17"
-ARG JDK_VERSION=11
+# This Dockerfile uses JDK17 by default.
+# To build with other versions, use "--build-arg JDK_VERSION=[value]"
+ARG JDK_VERSION=17
+ARG DSPACE_VERSION=latest
# Step 1 - Run Maven Build
# UMD Customization
-FROM docker.lib.umd.edu/drum-dependencies-7_x:latest as build
+FROM docker.lib.umd.edu/drum-dependencies:${DSPACE_VERSION} as build
# End UMD Customization
ARG TARGET_DIR=dspace-installer
WORKDIR /app
diff --git a/Dockerfile.dependencies b/Dockerfile.dependencies
index 30314035dba0..6f22911f62d9 100644
--- a/Dockerfile.dependencies
+++ b/Dockerfile.dependencies
@@ -1,12 +1,12 @@
# UMD Customization
-# This image will be published as docker.lib.umd.edu/drum-dependencies-7_x:latest
+# This image will be published as docker.lib.umd.edu/drum-dependencies-8_x:latest
# End UMD Customization
# The purpose of this image is to make the build for dspace/dspace run faster
#
-# This Dockerfile uses JDK11 by default, but has also been tested with JDK17.
-# To build with JDK17, use "--build-arg JDK_VERSION=17"
-ARG JDK_VERSION=11
+# This Dockerfile uses JDK17 by default.
+# To build with other versions, use "--build-arg JDK_VERSION=[value]"
+ARG JDK_VERSION=17
# Step 1 - Run Maven Build
FROM maven:3-eclipse-temurin-${JDK_VERSION} as build
diff --git a/Dockerfile.dev b/Dockerfile.dev
index b3161e48e152..80df4c1f1a76 100644
--- a/Dockerfile.dev
+++ b/Dockerfile.dev
@@ -1,65 +1,79 @@
-# This image will be published as dspace/dspace
-# See https://dspace-labs.github.io/DSpace-Docker-Images/ for usage details
-#
-# This version is JDK8 compatible
-# - tomcat:8-jre8
-# - ANT 1.10.7
-# - maven:3-jdk-8
-# - note:
-# - default tag for branch: dspace/dspace: dspace/dspace:dspace-6_x-jdk8
+# UMD-provided file running DSpace as part of a "Docker Compose" stack
+# This file is based on the stock "Dockerfile"
-ARG JDK_VERSION=11
+# This Dockerfile uses JDK17 by default.
+# To build with other versions, use "--build-arg JDK_VERSION=[value]"
+ARG JDK_VERSION=17
+ARG DSPACE_VERSION=latest
# Step 1 - Run Maven Build
-FROM docker.lib.umd.edu/drum-dependencies-7_x:latest as build
+# UMD Customization
+FROM docker.lib.umd.edu/drum-dependencies-8_x:${DSPACE_VERSION} as build
+# End UMD Customization
ARG TARGET_DIR=dspace-installer
WORKDIR /app
-
-# The dspace-install directory will be written to /install
+# The dspace-installer directory will be written to /install
RUN mkdir /install \
- && chown -Rv dspace: /install
-
+ && chown -Rv dspace: /install \
+ && chown -Rv dspace: /app
USER dspace
-
-# Copy the DSpace source code into the workdir (excluding .dockerignore contents)
+# Copy the DSpace source code (from local machine) into the workdir (excluding .dockerignore contents)
ADD --chown=dspace . /app/
-COPY dspace/src/main/docker/local.cfg /app/local.cfg
-
-# Build DSpace. Copy the dspace-install directory to /install. Clean up the build to keep the docker image small
-RUN mvn package -Pdspace-rest && \
+# 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.
+ENV MAVEN_FLAGS="-P-test-environment -Denforcer.skip=true -Dcheckstyle.skip=true -Dlicense.skip=true -Dxml.skip=true"
+RUN mvn --no-transfer-progress package ${MAVEN_FLAGS} && \
mv /app/dspace/target/${TARGET_DIR}/* /install && \
mvn clean
+# Remove the server webapp to keep image small.
+RUN rm -rf /install/webapps/server/
# Step 2 - Run Ant Deploy
+# UMD Customization
FROM docker.lib.umd.edu/drum-ant:latest as ant_build
+# End UMD Customization
ARG TARGET_DIR=dspace-installer
+# COPY the /install directory from 'build' container to /dspace-src in this container
COPY --from=build /install /dspace-src
WORKDIR /dspace-src
+# UMD Customization
+# Ant is installed as part of the "drum-ant" Docker image
+# End UMD Customization
+
+# Run necessary 'ant' deploy scripts
RUN ant init_installation update_configs update_code update_webapps
-# Step 3 - Run tomcat
-# Create a new tomcat image that does not retain the the build directory contents
-FROM tomcat:9-jdk${JDK_VERSION}
+# Step 3 - Start up DSpace via Runnable JAR
+FROM eclipse-temurin:${JDK_VERSION}
+# NOTE: DSPACE_INSTALL must align with the "dspace.dir" default configuration.
ENV DSPACE_INSTALL=/dspace
-ENV TOMCAT_INSTALL=/usr/local/tomcat
-# Copy the /dspace directory from 'ant_build' containger to /dspace in this container
+# Copy the /dspace directory from 'ant_build' container to /dspace in this container
COPY --from=ant_build /dspace $DSPACE_INSTALL
-# Enable the AJP connector in Tomcat's server.xml
-# NOTE: secretRequired="false" should only be used when AJP is NOT accessible from an external network. But, secretRequired="true" isn't supported by mod_proxy_ajp until Apache 2.5
-RUN sed -i '/Service name="Catalina".*/a \\n ' $TOMCAT_INSTALL/conf/server.xml
-# Expose Tomcat port and debug port
-EXPOSE 8080 8000
+WORKDIR $DSPACE_INSTALL
+# Expose Tomcat port
+EXPOSE 8080
# Give java extra memory (2GB)
ENV JAVA_OPTS=-Xmx2000m
-# 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
-# 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
+# UMD Customization
+ENV TZ=America/New_York
+RUN apt-get update && \
+ DEBIAN_FRONTEND=noninteractive apt-get install -y \
+ rsync \
+ cron \
+ csh \
+ postfix \
+ s-nail \
+ libgetopt-complete-perl \
+ libconfig-properties-perl \
+ vim \
+ python3-lxml && \
+ mkfifo /var/spool/postfix/public/pickup && \
+ ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
+# End UMD Customization
+# On startup, run DSpace Runnable JAR
+ENTRYPOINT ["java", "-jar", "webapps/server-boot.jar", "--dspace.dir=$DSPACE_INSTALL"]
diff --git a/Dockerfile.dev-additions b/Dockerfile.dev-additions
index 09b64ef09294..3f884a69cc67 100644
--- a/Dockerfile.dev-additions
+++ b/Dockerfile.dev-additions
@@ -1,51 +1,65 @@
-# This image will be published as dspace/dspace
-# See https://dspace-labs.github.io/DSpace-Docker-Images/ for usage details
-#
-# This version is JDK8 compatible
-# - tomcat:8-jre8
-# - ANT 1.10.7
-# - maven:3-jdk-8
-# - note:
-# - default tag for branch: dspace/dspace: dspace/dspace:dspace-6_x-jdk8
-#
-# Build: docker build -f Dockerfile.dev-additions -t docker.lib.umd.edu/drum:7_x-dev .
-#
-
-ARG JDK_VERSION=11
+# UMD-provided file running DSpace as part of a "Quick Build" setup
+# This file is based on "Dockerfile.dev"
+ARG JDK_VERSION=17
+ARG DSPACE_VERSION=latest
# Step 1 - Run Maven Build
-FROM docker.lib.umd.edu/drum:7_x-dev-base as build
+# UMD Customization
+FROM docker.lib.umd.edu/drum:8_x-dev-base as build
+# End UMD Customization
ARG TARGET_DIR=dspace-installer
WORKDIR /app
USER dspace
-# Copy the DSpace source code into the workdir (excluding .dockerignore contents)
+# UMD Customization
+# Copy the DSpace source code from the /dspace/modules/[additions]|[server]
+# directories from the local machine) into the workdir
+# (excluding .dockerignore contents)
RUN rm -rf /app/dspace/modules/additions /app/dspace/modules/server
ADD --chown=dspace dspace/modules/additions /app/dspace/modules/additions
ADD --chown=dspace dspace/modules/server /app/dspace/modules/server
-COPY dspace/src/main/docker/local.cfg /app/local.cfg
+# End UMD Customization
-# Build DSpace. Copy the dspace-install directory to /install. Clean up the build to keep the docker image small
-RUN mvn package -rf org.dspace:modules -pl '!org.dspace:dspace-iiif,!org.dspace:dspace-oai,!org.dspace:dspace-rdf,!org.dspace:dspace-sword,!org.dspace:dspace-swordv2' && \
-# RUN mvn package -Pdspace-rest && \
+# 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.
+ENV MAVEN_FLAGS="-P-test-environment -Denforcer.skip=true -Dcheckstyle.skip=true -Dlicense.skip=true -Dxml.skip=true"
+# UMD Customization
+RUN mvn package -rf org.dspace:modules -pl '!org.dspace:dspace-iiif,!org.dspace:dspace-oai,!org.dspace:dspace-rdf,!org.dspace:dspace-sword,!org.dspace:dspace-swordv2' ${MAVEN_FLAGS} && \
+# End UMD Customization
mv /app/dspace/target/${TARGET_DIR}/* /install && \
mvn clean
+# Remove the server webapp to keep image small.
+RUN rm -rf /install/webapps/server/
# Step 2 - Run Ant Deploy
+# UMD Customization
FROM docker.lib.umd.edu/drum-ant:latest as ant_build
+# End UMD Customization
ARG TARGET_DIR=dspace-installer
+# COPY the /install directory from 'build' container to /dspace-src in this container
COPY --from=build /install /dspace-src
WORKDIR /dspace-src
+# UMD Customization
+# Ant is installed as part of the "drum-ant" Docker image
+# End UMD Customization
+
+# Run necessary 'ant' deploy scripts
RUN ant init_installation update_configs update_code update_webapps
-# Step 3 - Run tomcat
-# Create a new tomcat image that does not retain the the build directory contents
-FROM tomcat:9-jdk${JDK_VERSION}
+# Step 3 - Start up DSpace via Runnable JAR
+FROM eclipse-temurin:${JDK_VERSION}
+# NOTE: DSPACE_INSTALL must align with the "dspace.dir" default configuration.
ENV DSPACE_INSTALL=/dspace
-ENV TOMCAT_INSTALL=/usr/local/tomcat
-
+# Copy the /dspace directory from 'ant_build' container to /dspace in this container
+COPY --from=ant_build /dspace $DSPACE_INSTALL
+WORKDIR $DSPACE_INSTALL
+# Expose Tomcat port
+EXPOSE 8080
+# Give java extra memory (2GB)
+ENV JAVA_OPTS=-Xmx2000m
# Add csh and Perl libraries for scripts in /dspace/bin
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y \
@@ -57,23 +71,5 @@ RUN apt-get update && \
RUN mkdir -p $DSPACE_INSTALL/proquest/incoming $DSPACE_INSTALL/proquest/processed \
$DSPACE_INSTALL/proquest/csv $DSPACE_INSTALL/proquest/marc
-# Copy the /dspace directory from 'ant_build' container to /dspace in this container
-COPY --from=ant_build /dspace $DSPACE_INSTALL
-# Enable the AJP connector in Tomcat's server.xml
-# NOTE: secretRequired="false" should only be used when AJP is NOT accessible from an external network. But, secretRequired="true" isn't supported by mod_proxy_ajp until Apache 2.5
-RUN sed -i '/Service name="Catalina".*/a \\n ' $TOMCAT_INSTALL/conf/server.xml
-# Expose Tomcat port and debug port
-EXPOSE 8080 8000
-# Give java extra memory (2GB)
-ENV JAVA_OPTS=-Xmx2000m
-
-# 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
-# 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
-
+# On startup, run DSpace Runnable JAR
+ENTRYPOINT ["java", "-jar", "webapps/server-boot.jar", "--dspace.dir=$DSPACE_INSTALL"]
diff --git a/Dockerfile.dev-base b/Dockerfile.dev-base
index 6b0e1f6cbc7f..64b9aaff17cf 100644
--- a/Dockerfile.dev-base
+++ b/Dockerfile.dev-base
@@ -1,34 +1,32 @@
-# This image will be published as dspace/dspace
-# See https://dspace-labs.github.io/DSpace-Docker-Images/ for usage details
-#
-# This version is JDK8 compatible
-# - maven:3-jdk-8
-# - note:
-# - default tag: docker.lib.umd.edu/drum:7_x-dev-base
-#
-# Build: docker build -f Dockerfile.dev-base -t docker.lib.umd.edu/drum:7_x-dev-base .
-#
-
-ARG JDK_VERSION=11
+# UMD-provided file running DSpace as part of a "Quick Build" setup
+# This file is based on "Dockerfile.dev"
+ARG JDK_VERSION=17
+ARG DSPACE_VERSION=latest
# Step 1 - Run Maven Build
-FROM docker.lib.umd.edu/drum-dependencies-7_x:latest
+# UMD Customization
+FROM docker.lib.umd.edu/drum-dependencies-8_x:${DSPACE_VERSION} as build
+# End UMD Customization
ARG TARGET_DIR=dspace-installer
WORKDIR /app
-
-# The dspace-install directory will be written to /install
+# The dspace-installer directory will be written to /install
RUN mkdir /install \
- && chown -Rv dspace: /install
-
+ && chown -Rv dspace: /install \
+ && chown -Rv dspace: /app
USER dspace
-
-# Copy the DSpace source code into the workdir (excluding .dockerignore contents)
+# Copy the DSpace source code (from local machine) into the workdir (excluding .dockerignore contents)
ADD --chown=dspace . /app/
-COPY dspace/src/main/docker/local.cfg /app/local.cfg
+# 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.
+ENV MAVEN_FLAGS="-P-test-environment -Denforcer.skip=true -Dcheckstyle.skip=true -Dlicense.skip=true -Dxml.skip=true"
+# UMD Customization
+RUN mvn install ${MAVEN_FLAGS} && \
+ mvn clean
+# End UMD Customization
-# Build DSpace.
-# RUN mvn package
-RUN mvn install -Dlicense.skip=true -Dcheckstyle.skip
+# Remove the server webapp to keep image small.
+RUN rm -rf /install/webapps/server/
-# RUN mvn install -pl '!org.dspace.modules:additions'
-# RUN mvn package -rf org.dspace.modules:additions
+# UMD Customization - Remaining steps are handled by Dockerfile.dev-additions
diff --git a/Dockerfile.test b/Dockerfile.test
index f6f8c1a290f9..cdfd5e83af5f 100644
--- a/Dockerfile.test
+++ b/Dockerfile.test
@@ -1,16 +1,17 @@
# This image will be published as dspace/dspace
# See https://github.com/DSpace/DSpace/tree/main/dspace/src/main/docker for usage details
#
-# - note: default tag for branch: dspace/dspace: dspace/dspace:dspace-7_x-test
+# - note: default tag for branch: dspace/dspace: dspace/dspace:latest-test
#
# This image is meant for TESTING/DEVELOPMENT ONLY as it deploys the old v6 REST API under HTTP (not HTTPS)
-# This Dockerfile uses JDK11 by default, but has also been tested with JDK17.
-# To build with JDK17, use "--build-arg JDK_VERSION=17"
-ARG JDK_VERSION=11
+# This Dockerfile uses JDK17 by default.
+# To build with other versions, use "--build-arg JDK_VERSION=[value]"
+ARG JDK_VERSION=17
+ARG DSPACE_VERSION=latest
# Step 1 - Run Maven Build
-FROM dspace/dspace-dependencies:dspace-7_x as build
+FROM dspace/dspace-dependencies:${DSPACE_VERSION} as build
ARG TARGET_DIR=dspace-installer
WORKDIR /app
# The dspace-installer directory will be written to /install
@@ -20,11 +21,13 @@ 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
+# Remove the server webapp to keep image small. Rename runnable JAR to server-boot.jar.
+RUN rm -rf /install/webapps/server/
# Step 2 - Run Ant Deploy
FROM eclipse-temurin:${JDK_VERSION} as ant_build
@@ -47,36 +50,18 @@ RUN mkdir $ANT_HOME && \
# Run necessary 'ant' deploy scripts
RUN ant init_installation update_configs update_code update_webapps
-# Step 3 - Run tomcat
-# Create a new tomcat image that does not retain the the build directory contents
-FROM tomcat:9-jdk${JDK_VERSION}
+# Step 3 - Start up DSpace via Runnable JAR
+FROM eclipse-temurin:${JDK_VERSION}
+# NOTE: DSPACE_INSTALL must align with the "dspace.dir" default configuration.
ENV DSPACE_INSTALL=/dspace
-ENV TOMCAT_INSTALL=/usr/local/tomcat
-# Copy the /dspace directory from 'ant_build' containger to /dspace in this container
+# Copy the /dspace directory from 'ant_build' container to /dspace in this container
COPY --from=ant_build /dspace $DSPACE_INSTALL
-# Enable the AJP connector in Tomcat's server.xml
-# NOTE: secretRequired="false" should only be used when AJP is NOT accessible from an external network. But, secretRequired="true" isn't supported by mod_proxy_ajp until Apache 2.5
-RUN sed -i '/Service name="Catalina".*/a \\n ' $TOMCAT_INSTALL/conf/server.xml
-# Expose Tomcat port and AJP port
-EXPOSE 8080 8009 8000
+WORKDIR $DSPACE_INSTALL
+# Expose Tomcat port and debugging port
+EXPOSE 8080 8000
# Give java extra memory (2GB)
ENV JAVA_OPTS=-Xmx2000m
# Set up debugging
ENV CATALINA_OPTS=-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:8000
-
-# 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
-# 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
-
-# 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
+# On startup, run DSpace Runnable JAR
+ENTRYPOINT ["java", "-jar", "webapps/server-boot.jar", "--dspace.dir=$DSPACE_INSTALL"]
diff --git a/LICENSES_THIRD_PARTY b/LICENSES_THIRD_PARTY
index 791009e4c3ae..d7e928147c89 100644
--- a/LICENSES_THIRD_PARTY
+++ b/LICENSES_THIRD_PARTY
@@ -25,6 +25,7 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
* AWS Java SDK for AWS KMS (com.amazonaws:aws-java-sdk-kms:1.12.261 - https://aws.amazon.com/sdkforjava)
* AWS Java SDK for Amazon S3 (com.amazonaws:aws-java-sdk-s3:1.12.261 - https://aws.amazon.com/sdkforjava)
* JMES Path Query library (com.amazonaws:jmespath-java:1.12.261 - https://aws.amazon.com/sdkforjava)
+ * Titanium JSON-LD 1.1 (JRE11) (com.apicatalog:titanium-json-ld:1.3.2 - https://github.com/filip26/titanium-json-ld)
* HPPC Collections (com.carrotsearch:hppc:0.8.1 - http://labs.carrotsearch.com/hppc.html/hppc)
* com.drewnoakes:metadata-extractor (com.drewnoakes:metadata-extractor:2.19.0 - https://drewnoakes.com/code/exif/)
* parso (com.epam:parso:2.0.14 - https://github.com/epam/parso)
@@ -34,15 +35,19 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
* jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.16.0 - https://github.com/FasterXML/jackson)
* Jackson dataformat: CBOR (com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.12.6 - http://github.com/FasterXML/jackson-dataformats-binary)
* Jackson dataformat: Smile (com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.15.2 - https://github.com/FasterXML/jackson-dataformats-binary)
- * Jackson-dataformat-YAML (com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.1 - https://github.com/FasterXML/jackson-dataformats-text)
- * Jackson datatype: jdk8 (com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.5 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8)
- * Jackson datatype: JSR310 (com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.11.1 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310)
- * Jackson datatype: JSR310 (com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.5 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310)
- * Jackson-module-parameter-names (com.fasterxml.jackson.module:jackson-module-parameter-names:2.13.5 - https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names)
+ * Jackson-dataformat-TOML (com.fasterxml.jackson.dataformat:jackson-dataformat-toml:2.15.2 - https://github.com/FasterXML/jackson-dataformats-text)
+ * Jackson-dataformat-YAML (com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.16.2 - https://github.com/FasterXML/jackson-dataformats-text)
+ * Jackson datatype: jdk8 (com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.15.4 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8)
+ * Jackson datatype: JSR310 (com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.0 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310)
+ * Jackson Jakarta-RS: base (com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-base:2.16.2 - https://github.com/FasterXML/jackson-jakarta-rs-providers/jackson-jakarta-rs-base)
+ * Jackson Jakarta-RS: JSON (com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-json-provider:2.16.2 - https://github.com/FasterXML/jackson-jakarta-rs-providers/jackson-jakarta-rs-json-provider)
+ * Jackson module: Jakarta XML Bind Annotations (jakarta.xml.bind) (com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.16.2 - https://github.com/FasterXML/jackson-modules-base)
+ * Jackson-module-parameter-names (com.fasterxml.jackson.module:jackson-module-parameter-names:2.15.4 - https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names)
* Java UUID Generator (com.fasterxml.uuid:java-uuid-generator:4.0.1 - https://github.com/cowtowncoder/java-uuid-generator)
* Woodstox (com.fasterxml.woodstox:woodstox-core:6.5.1 - https://github.com/FasterXML/woodstox)
* zjsonpatch (com.flipkart.zjsonpatch:zjsonpatch:0.4.16 - https://github.com/flipkart-incubator/zjsonpatch/)
* Caffeine cache (com.github.ben-manes.caffeine:caffeine:2.9.3 - https://github.com/ben-manes/caffeine)
+ * Caffeine cache (com.github.ben-manes.caffeine:caffeine:3.1.6 - https://github.com/ben-manes/caffeine)
* btf (com.github.java-json-tools:btf:1.3 - https://github.com/java-json-tools/btf)
* jackson-coreutils (com.github.java-json-tools:jackson-coreutils:2.0 - https://github.com/java-json-tools/jackson-coreutils)
* jackson-coreutils-equivalence (com.github.java-json-tools:jackson-coreutils-equivalence:1.0 - https://github.com/java-json-tools/jackson-coreutils)
@@ -54,8 +59,8 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
* Google APIs Client Library for Java (com.google.api-client:google-api-client:1.23.0 - https://github.com/google/google-api-java-client/google-api-client)
* Google Analytics API v3-rev145-1.23.0 (com.google.apis:google-api-services-analytics:v3-rev145-1.23.0 - http://nexus.sonatype.org/oss-repository-hosting.html/google-api-services-analytics)
* FindBugs-jsr305 (com.google.code.findbugs:jsr305:3.0.2 - http://findbugs.sourceforge.net/)
- * Gson (com.google.code.gson:gson:2.9.0 - https://github.com/google/gson/gson)
- * error-prone annotations (com.google.errorprone:error_prone_annotations:2.18.0 - https://errorprone.info/error_prone_annotations)
+ * Gson (com.google.code.gson:gson:2.10.1 - https://github.com/google/gson/gson)
+ * error-prone annotations (com.google.errorprone:error_prone_annotations:2.10.0 - https://errorprone.info/error_prone_annotations)
* Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.1 - https://github.com/google/guava/failureaccess)
* Guava: Google Core Libraries for Java (com.google.guava:guava:32.0.0-jre - https://github.com/google/guava)
* Guava: Google Core Libraries for Java (JDK5 Backport) (com.google.guava:guava-jdk5:17.0 - http://code.google.com/p/guava-libraries/guava-jdk5)
@@ -75,7 +80,7 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
* Disruptor Framework (com.lmax:disruptor:3.4.2 - http://lmax-exchange.github.com/disruptor)
* MaxMind DB Reader (com.maxmind.db:maxmind-db:2.1.0 - http://dev.maxmind.com/)
* MaxMind GeoIP2 API (com.maxmind.geoip2:geoip2:2.17.0 - https://dev.maxmind.com/geoip?lang=en)
- * Nimbus JOSE+JWT (com.nimbusds:nimbus-jose-jwt:7.9 - https://bitbucket.org/connect2id/nimbus-jose-jwt)
+ * Nimbus JOSE+JWT (com.nimbusds:nimbus-jose-jwt:9.37.3 - https://bitbucket.org/connect2id/nimbus-jose-jwt)
* opencsv (com.opencsv:opencsv:5.9 - http://opencsv.sf.net)
* java-libpst (com.pff:java-libpst:0.9.3 - https://github.com/rjohnsondev/java-libpst)
* rome (com.rometools:rome:1.19.0 - http://rometools.com/rome)
@@ -98,19 +103,23 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
* Apache Commons Codec (commons-codec:commons-codec:1.16.0 - https://commons.apache.org/proper/commons-codec/)
* Apache Commons Collections (commons-collections:commons-collections:3.2.2 - http://commons.apache.org/collections/)
* Commons Digester (commons-digester:commons-digester:2.1 - http://commons.apache.org/digester/)
- * Apache Commons FileUpload (commons-fileupload:commons-fileupload:1.5 - https://commons.apache.org/proper/commons-fileupload/)
* Apache Commons IO (commons-io:commons-io:2.15.1 - https://commons.apache.org/proper/commons-io/)
* Commons Lang (commons-lang:commons-lang:2.6 - http://commons.apache.org/lang/)
* Apache Commons Logging (commons-logging:commons-logging:1.3.0 - https://commons.apache.org/proper/commons-logging/)
* Apache Commons Validator (commons-validator:commons-validator:1.7 - http://commons.apache.org/proper/commons-validator/)
* GeoJson POJOs for Jackson (de.grundid.opendatalab:geojson-jackson:1.14 - https://github.com/opendatalab-de/geojson-jackson)
+ * broker-client (eu.openaire:broker-client:1.1.2 - http://api.openaire.eu/broker/broker-client)
* OpenAIRE Funders Model (eu.openaire:funders-model:2.0.0 - https://api.openaire.eu)
* Metrics Core (io.dropwizard.metrics:metrics-core:4.1.5 - https://metrics.dropwizard.io/metrics-core)
* Graphite Integration for Metrics (io.dropwizard.metrics:metrics-graphite:4.1.5 - https://metrics.dropwizard.io/metrics-graphite)
* Metrics Integration for Jetty 9.3 and higher (io.dropwizard.metrics:metrics-jetty9:4.1.5 - https://metrics.dropwizard.io/metrics-jetty9)
* Metrics Integration with JMX (io.dropwizard.metrics:metrics-jmx:4.1.5 - https://metrics.dropwizard.io/metrics-jmx)
* JVM Integration for Metrics (io.dropwizard.metrics:metrics-jvm:4.1.5 - https://metrics.dropwizard.io/metrics-jvm)
- * micrometer-core (io.micrometer:micrometer-core:1.9.17 - https://github.com/micrometer-metrics/micrometer)
+ * SWORD v2 Common Server Library (forked) (io.gdcc:sword2-server:2.0.0 - https://github.com/gdcc/sword2-server)
+ * micrometer-commons (io.micrometer:micrometer-commons:1.12.6 - https://github.com/micrometer-metrics/micrometer)
+ * micrometer-core (io.micrometer:micrometer-core:1.12.6 - https://github.com/micrometer-metrics/micrometer)
+ * micrometer-jakarta9 (io.micrometer:micrometer-jakarta9:1.12.6 - https://github.com/micrometer-metrics/micrometer)
+ * micrometer-observation (io.micrometer:micrometer-observation:1.12.6 - https://github.com/micrometer-metrics/micrometer)
* Netty/Buffer (io.netty:netty-buffer:4.1.106.Final - https://netty.io/netty-buffer/)
* Netty/Buffer (io.netty:netty-buffer:4.1.99.Final - https://netty.io/netty-buffer/)
* Netty/Codec (io.netty:netty-codec:4.1.106.Final - https://netty.io/netty-codec/)
@@ -132,22 +141,29 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
* OpenTracing-noop (io.opentracing:opentracing-noop:0.33.0 - https://github.com/opentracing/opentracing-java/opentracing-noop)
* OpenTracing-util (io.opentracing:opentracing-util:0.33.0 - https://github.com/opentracing/opentracing-java/opentracing-util)
* Google S2 geometry library (io.sgr:s2-geometry-library-java:1.0.0 - https://github.com/sgr-io/s2-geometry-library-java)
+ * Jandex: Core (io.smallrye:jandex:3.1.2 - https://smallrye.io)
* swagger-annotations (io.swagger:swagger-annotations:1.6.2 - https://github.com/swagger-api/swagger-core/modules/swagger-annotations)
* swagger-compat-spec-parser (io.swagger:swagger-compat-spec-parser:1.0.52 - http://nexus.sonatype.org/oss-repository-hosting.html/swagger-parser-project/modules/swagger-compat-spec-parser)
* swagger-core (io.swagger:swagger-core:1.6.2 - https://github.com/swagger-api/swagger-core/modules/swagger-core)
* swagger-models (io.swagger:swagger-models:1.6.2 - https://github.com/swagger-api/swagger-core/modules/swagger-models)
* swagger-parser (io.swagger:swagger-parser:1.0.52 - http://nexus.sonatype.org/oss-repository-hosting.html/swagger-parser-project/modules/swagger-parser)
* swagger-annotations (io.swagger.core.v3:swagger-annotations:2.1.5 - https://github.com/swagger-api/swagger-core/modules/swagger-annotations)
+ * swagger-annotations-jakarta (io.swagger.core.v3:swagger-annotations-jakarta:2.2.21 - https://github.com/swagger-api/swagger-core/modules/swagger-annotations-jakarta)
* swagger-core (io.swagger.core.v3:swagger-core:2.1.5 - https://github.com/swagger-api/swagger-core/modules/swagger-core)
+ * swagger-core-jakarta (io.swagger.core.v3:swagger-core-jakarta:2.2.21 - https://github.com/swagger-api/swagger-core/modules/swagger-core-jakarta)
+ * swagger-integration-jakarta (io.swagger.core.v3:swagger-integration-jakarta:2.2.21 - https://github.com/swagger-api/swagger-core/modules/swagger-integration-jakarta)
+ * swagger-jaxrs2-jakarta (io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.21 - https://github.com/swagger-api/swagger-core/modules/swagger-jaxrs2-jakarta)
* swagger-models (io.swagger.core.v3:swagger-models:2.1.5 - https://github.com/swagger-api/swagger-core/modules/swagger-models)
+ * swagger-models-jakarta (io.swagger.core.v3:swagger-models-jakarta:2.2.21 - https://github.com/swagger-api/swagger-core/modules/swagger-models-jakarta)
* swagger-parser (io.swagger.parser.v3:swagger-parser:2.0.23 - http://nexus.sonatype.org/oss-repository-hosting.html/swagger-parser-project/modules/swagger-parser)
* swagger-parser (io.swagger.parser.v3:swagger-parser-core:2.0.23 - http://nexus.sonatype.org/oss-repository-hosting.html/swagger-parser-project/modules/swagger-parser-core)
* swagger-parser-v2-converter (io.swagger.parser.v3:swagger-parser-v2-converter:2.0.23 - http://nexus.sonatype.org/oss-repository-hosting.html/swagger-parser-project/modules/swagger-parser-v2-converter)
* swagger-parser-v3 (io.swagger.parser.v3:swagger-parser-v3:2.0.23 - http://nexus.sonatype.org/oss-repository-hosting.html/swagger-parser-project/modules/swagger-parser-v3)
- * Jakarta Bean Validation API (jakarta.validation:jakarta.validation-api:2.0.2 - https://beanvalidation.org)
+ * Jakarta Dependency Injection (jakarta.inject:jakarta.inject-api:2.0.1 - https://github.com/eclipse-ee4j/injection-api)
+ * Jakarta Bean Validation API (jakarta.validation:jakarta.validation-api:3.0.2 - https://beanvalidation.org)
* JSR107 API and SPI (javax.cache:cache-api:1.1.1 - https://github.com/jsr107/jsr107spec)
* javax.inject (javax.inject:javax.inject:1 - http://code.google.com/p/atinject/)
- * Bean Validation API (javax.validation:validation-api:2.0.1.Final - http://beanvalidation.org)
+ * Bean Validation API (javax.validation:validation-api:1.1.0.Final - http://beanvalidation.org)
* jdbm (jdbm:jdbm:1.0 - no url defined)
* Joda-Time (joda-time:joda-time:2.12.5 - https://www.joda.org/joda-time/)
* Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.11.13 - https://bytebuddy.net/byte-buddy)
@@ -155,12 +171,11 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
* eigenbase-properties (net.hydromatic:eigenbase-properties:1.1.5 - http://github.com/julianhyde/eigenbase-properties)
* json-unit-core (net.javacrumbs.json-unit:json-unit-core:2.19.0 - https://github.com/lukas-krecan/JsonUnit/json-unit-core)
* "Java Concurrency in Practice" book annotations (net.jcip:jcip-annotations:1.0 - http://jcip.net/)
- * ASM based accessors helper used by json-smart (net.minidev:accessors-smart:1.2 - http://www.minidev.net/)
* ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.5.0 - https://urielch.github.io/)
- * JSON Small and Fast Parser (net.minidev:json-smart:2.3 - http://www.minidev.net/)
* JSON Small and Fast Parser (net.minidev:json-smart:2.5.0 - https://urielch.github.io/)
* Abdera Core (org.apache.abdera:abdera-core:1.1.3 - http://abdera.apache.org/abdera-core)
* I18N Libraries (org.apache.abdera:abdera-i18n:1.1.3 - http://abdera.apache.org)
+ * Abdera Parser (org.apache.abdera:abdera-parser:1.1.3 - http://abdera.apache.org/abdera-parser)
* Apache Ant Core (org.apache.ant:ant:1.10.14 - https://ant.apache.org/)
* Apache Ant Launcher (org.apache.ant:ant-launcher:1.10.14 - https://ant.apache.org/)
* Apache Commons BCEL (org.apache.bcel:bcel:6.7.0 - https://commons.apache.org/proper/commons-bcel)
@@ -188,19 +203,33 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
* Apache Hadoop HDFS Client (org.apache.hadoop:hadoop-hdfs-client:3.2.4 - no url defined)
* htrace-core4 (org.apache.htrace:htrace-core4:4.1.0-incubating - http://incubator.apache.org/projects/htrace.html)
* Apache HttpClient (org.apache.httpcomponents:httpclient:4.5.14 - http://hc.apache.org/httpcomponents-client-ga)
- * Apache HttpClient Cache (org.apache.httpcomponents:httpclient-cache:4.2.6 - http://hc.apache.org/httpcomponents-client)
+ * Apache HttpClient Cache (org.apache.httpcomponents:httpclient-cache:4.5.14 - http://hc.apache.org/httpcomponents-client-ga)
* Apache HttpCore (org.apache.httpcomponents:httpcore:4.4.16 - http://hc.apache.org/httpcomponents-core-ga)
* Apache HttpClient Mime (org.apache.httpcomponents:httpmime:4.5.14 - http://hc.apache.org/httpcomponents-client-ga)
* Apache HttpClient (org.apache.httpcomponents.client5:httpclient5:5.1.3 - https://hc.apache.org/httpcomponents-client-5.0.x/5.1.3/httpclient5/)
+ * Apache HttpClient (org.apache.httpcomponents.client5:httpclient5:5.3.1 - https://hc.apache.org/httpcomponents-client-5.0.x/5.3.1/httpclient5/)
* Apache HttpComponents Core HTTP/1.1 (org.apache.httpcomponents.core5:httpcore5:5.1.3 - https://hc.apache.org/httpcomponents-core-5.1.x/5.1.3/httpcore5/)
+ * Apache HttpComponents Core HTTP/1.1 (org.apache.httpcomponents.core5:httpcore5:5.2.4 - https://hc.apache.org/httpcomponents-core-5.2.x/5.2.4/httpcore5/)
* Apache HttpComponents Core HTTP/2 (org.apache.httpcomponents.core5:httpcore5-h2:5.1.3 - https://hc.apache.org/httpcomponents-core-5.1.x/5.1.3/httpcore5-h2/)
+ * Apache HttpComponents Core HTTP/2 (org.apache.httpcomponents.core5:httpcore5-h2:5.2.4 - https://hc.apache.org/httpcomponents-core-5.2.x/5.2.4/httpcore5-h2/)
* Apache James :: Mime4j :: Core (org.apache.james:apache-mime4j-core:0.8.10 - http://james.apache.org/mime4j/apache-mime4j-core)
* Apache James :: Mime4j :: DOM (org.apache.james:apache-mime4j-dom:0.8.11 - http://james.apache.org/mime4j/apache-mime4j-dom)
- * Apache Jena - Libraries POM (org.apache.jena:apache-jena-libs:2.13.0 - http://jena.apache.org/apache-jena-libs/)
- * Apache Jena - ARQ (SPARQL 1.1 Query Engine) (org.apache.jena:jena-arq:2.13.0 - http://jena.apache.org/jena-arq/)
- * Apache Jena - Core (org.apache.jena:jena-core:2.13.0 - http://jena.apache.org/jena-core/)
- * Apache Jena - IRI (org.apache.jena:jena-iri:1.1.2 - http://jena.apache.org/jena-iri/)
- * Apache Jena - TDB (Native Triple Store) (org.apache.jena:jena-tdb:1.1.2 - http://jena.apache.org/jena-tdb/)
+ * Apache Jena - Libraries POM (org.apache.jena:apache-jena-libs:4.9.0 - https://jena.apache.org/apache-jena-libs/)
+ * Apache Jena - ARQ (org.apache.jena:jena-arq:4.9.0 - https://jena.apache.org/jena-arq/)
+ * Apache Jena - Base (org.apache.jena:jena-base:4.9.0 - https://jena.apache.org/jena-base/)
+ * Apache Jena - Core (org.apache.jena:jena-core:4.9.0 - https://jena.apache.org/jena-core/)
+ * Apache Jena - DBOE Base (org.apache.jena:jena-dboe-base:4.9.0 - https://jena.apache.org/jena-dboe-base/)
+ * Apache Jena - DBOE Indexes (org.apache.jena:jena-dboe-index:4.9.0 - https://jena.apache.org/jena-dboe-index/)
+ * Apache Jena - DBOE Storage (org.apache.jena:jena-dboe-storage:4.9.0 - https://jena.apache.org/jena-dboe-storage/)
+ * Apache Jena - DBOE Transactional Datastructures (org.apache.jena:jena-dboe-trans-data:4.9.0 - https://jena.apache.org/jena-dboe-trans-data/)
+ * Apache Jena - DBOE Transactions (org.apache.jena:jena-dboe-transaction:4.9.0 - https://jena.apache.org/jena-dboe-transaction/)
+ * Apache Jena - IRI (org.apache.jena:jena-iri:4.9.0 - https://jena.apache.org/jena-iri/)
+ * Apache Jena - RDF Connection (org.apache.jena:jena-rdfconnection:4.9.0 - https://jena.apache.org/jena-rdfconnection/)
+ * Apache Jena - RDF Patch (org.apache.jena:jena-rdfpatch:4.9.0 - https://jena.apache.org/jena-rdfpatch/)
+ * Apache Jena - SHACL (org.apache.jena:jena-shacl:4.9.0 - https://jena.apache.org/jena-shacl/)
+ * Apache Jena - ShEx (org.apache.jena:jena-shex:4.9.0 - https://jena.apache.org/jena-shex/)
+ * Apache Jena - TDB1 (Native Triple Store) (org.apache.jena:jena-tdb:4.9.0 - https://jena.apache.org/jena-tdb/)
+ * Apache Jena - TDB2 (Native Triple Store) (org.apache.jena:jena-tdb2:4.9.0 - https://jena.apache.org/jena-tdb2/)
* Kerby-kerb core (org.apache.kerby:kerb-core:1.0.1 - http://directory.apache.org/kerby/kerby-kerb/kerb-core)
* Kerby-kerb Util (org.apache.kerby:kerb-util:1.0.1 - http://directory.apache.org/kerby/kerby-kerb/kerb-util)
* Kerby ASN1 Project (org.apache.kerby:kerby-asn1:1.0.1 - http://directory.apache.org/kerby/kerby-common/kerby-asn1)
@@ -211,6 +240,7 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
* Apache Log4j JUL Adapter (org.apache.logging.log4j:log4j-jul:2.23.1 - https://logging.apache.org/log4j/2.x/log4j/log4j-jul/)
* Apache Log4j Layout for JSON template (org.apache.logging.log4j:log4j-layout-template-json:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-layout-template-json/)
* Apache Log4j SLF4J Binding (org.apache.logging.log4j:log4j-slf4j-impl:2.23.1 - https://logging.apache.org/log4j/2.x/log4j/log4j-slf4j-impl/)
+ * Apache Log4j SLF4J 2.0 Binding (org.apache.logging.log4j:log4j-slf4j2-impl:2.21.1 - https://logging.apache.org/log4j/2.x/log4j/log4j-slf4j2-impl/)
* Apache Log4j Web (org.apache.logging.log4j:log4j-web:2.23.1 - https://logging.apache.org/log4j/2.x/log4j/log4j-web/)
* Lucene Common Analyzers (org.apache.lucene:lucene-analyzers-common:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-analyzers-common)
* Lucene ICU Analysis Components (org.apache.lucene:lucene-analyzers-icu:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-analyzers-icu)
@@ -249,7 +279,7 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
* Apache Solr Solrj (org.apache.solr:solr-solrj:8.11.3 - https://lucene.apache.org/solr-parent/solr-solrj)
* Apache Standard Taglib Implementation (org.apache.taglibs:taglibs-standard-impl:1.2.5 - http://tomcat.apache.org/taglibs/standard-1.2.5/taglibs-standard-impl)
* Apache Standard Taglib Specification API (org.apache.taglibs:taglibs-standard-spec:1.2.5 - http://tomcat.apache.org/taglibs/standard-1.2.5/taglibs-standard-spec)
- * Apache Thrift (org.apache.thrift:libthrift:0.9.2 - http://thrift.apache.org)
+ * Apache Thrift (org.apache.thrift:libthrift:0.18.1 - http://thrift.apache.org)
* Apache Tika core (org.apache.tika:tika-core:2.9.2 - https://tika.apache.org/)
* Apache Tika Apple parser module (org.apache.tika:tika-parser-apple-module:2.9.2 - https://tika.apache.org/tika-parser-apple-module/)
* Apache Tika audiovideo parser module (org.apache.tika:tika-parser-audiovideo-module:2.9.2 - https://tika.apache.org/tika-parser-audiovideo-module/)
@@ -274,34 +304,37 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
* Apache Tika XMP commons (org.apache.tika:tika-parser-xmp-commons:2.9.2 - https://tika.apache.org/tika-parser-xmp-commons/)
* Apache Tika ZIP commons (org.apache.tika:tika-parser-zip-commons:2.9.2 - https://tika.apache.org/tika-parser-zip-commons/)
* Apache Tika standard parser package (org.apache.tika:tika-parsers-standard-package:2.9.2 - https://tika.apache.org/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/)
- * tomcat-embed-core (org.apache.tomcat.embed:tomcat-embed-core:9.0.83 - https://tomcat.apache.org/)
- * tomcat-embed-el (org.apache.tomcat.embed:tomcat-embed-el:9.0.83 - https://tomcat.apache.org/)
- * tomcat-embed-websocket (org.apache.tomcat.embed:tomcat-embed-websocket:9.0.83 - https://tomcat.apache.org/)
+ * tomcat-embed-core (org.apache.tomcat.embed:tomcat-embed-core:10.1.24 - https://tomcat.apache.org/)
+ * tomcat-embed-el (org.apache.tomcat.embed:tomcat-embed-el:10.1.24 - https://tomcat.apache.org/)
+ * tomcat-embed-websocket (org.apache.tomcat.embed:tomcat-embed-websocket:10.1.24 - https://tomcat.apache.org/)
* Apache Velocity - Engine (org.apache.velocity:velocity-engine-core:2.3 - http://velocity.apache.org/engine/devel/velocity-engine-core/)
* Apache Velocity - JSR 223 Scripting (org.apache.velocity:velocity-engine-scripting:2.2 - http://velocity.apache.org/engine/devel/velocity-engine-scripting/)
- * Axiom API (org.apache.ws.commons.axiom:axiom-api:1.2.22 - http://ws.apache.org/axiom/)
- * Abdera Model (FOM) Implementation (org.apache.ws.commons.axiom:fom-impl:1.2.22 - http://ws.apache.org/axiom/implementations/fom-impl/)
+ * Axiom API (org.apache.ws.commons.axiom:axiom-api:1.2.14 - http://ws.apache.org/axiom/)
+ * Axiom Impl (org.apache.ws.commons.axiom:axiom-impl:1.2.14 - http://ws.apache.org/axiom/)
* XmlBeans (org.apache.xmlbeans:xmlbeans:5.2.0 - https://xmlbeans.apache.org/)
* Apache ZooKeeper - Server (org.apache.zookeeper:zookeeper:3.6.2 - http://zookeeper.apache.org/zookeeper)
* Apache ZooKeeper - Jute (org.apache.zookeeper:zookeeper-jute:3.6.2 - http://zookeeper.apache.org/zookeeper-jute)
* org.apiguardian:apiguardian-api (org.apiguardian:apiguardian-api:1.1.2 - https://github.com/apiguardian-team/apiguardian)
- * AssertJ fluent assertions (org.assertj:assertj-core:3.22.0 - https://assertj.github.io/doc/assertj-core/)
+ * AssertJ Core (org.assertj:assertj-core:3.24.2 - https://assertj.github.io/doc/#assertj-core)
* Evo Inflector (org.atteo:evo-inflector:1.3 - http://atteo.org/static/evo-inflector)
+ * Awaitility (org.awaitility:awaitility:4.2.1 - http://awaitility.org)
* jose4j (org.bitbucket.b_c:jose4j:0.6.5 - https://bitbucket.org/b_c/jose4j/)
* TagSoup (org.ccil.cowan.tagsoup:tagsoup:1.2.1 - http://home.ccil.org/~cowan/XML/tagsoup/)
+ * Woodstox (org.codehaus.woodstox:wstx-asl:3.2.6 - http://woodstox.codehaus.org)
* jems (org.dmfs:jems:1.18 - https://github.com/dmfs/jems)
* rfc3986-uri (org.dmfs:rfc3986-uri:0.8.1 - https://github.com/dmfs/uri-toolkit)
* Jetty :: Apache JSP Implementation (org.eclipse.jetty:apache-jsp:9.4.15.v20190215 - http://www.eclipse.org/jetty)
* Apache :: JSTL module (org.eclipse.jetty:apache-jstl:9.4.15.v20190215 - http://tomcat.apache.org/taglibs/standard/)
* Jetty :: ALPN :: Client (org.eclipse.jetty:jetty-alpn-client:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-client)
* Jetty :: ALPN :: JDK9 Client Implementation (org.eclipse.jetty:jetty-alpn-java-client:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-java-client)
- * Jetty :: ALPN :: JDK9 Server Implementation (org.eclipse.jetty:jetty-alpn-java-server:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-java-server)
+ * Jetty :: ALPN :: JDK9 Server Implementation (org.eclipse.jetty:jetty-alpn-java-server:9.4.15.v20190215 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-java-server)
+ * Jetty :: ALPN :: JDK9 Server Implementation (org.eclipse.jetty:jetty-alpn-java-server:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-java-server)
+ * Jetty :: ALPN :: Server (org.eclipse.jetty:jetty-alpn-server:9.4.15.v20190215 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-server)
* Jetty :: ALPN :: Server (org.eclipse.jetty:jetty-alpn-server:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-server)
- * Jetty :: ALPN :: Server (org.eclipse.jetty:jetty-alpn-server:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-server)
* Jetty :: Servlet Annotations (org.eclipse.jetty:jetty-annotations:9.4.15.v20190215 - http://www.eclipse.org/jetty)
* Jetty :: Asynchronous HTTP Client (org.eclipse.jetty:jetty-client:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-client)
+ * Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.15.v20190215 - http://www.eclipse.org/jetty)
* Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-continuation)
- * Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-continuation)
* Jetty :: Deployers (org.eclipse.jetty:jetty-deploy:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-deploy)
* Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-http)
* Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-io)
@@ -313,31 +346,33 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
* Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-security)
* Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-server)
* Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-servlet)
- * Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-servlets)
+ * Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.4.15.v20190215 - http://www.eclipse.org/jetty)
+ * Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-servlets)
* Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-util)
* Jetty :: Utilities :: Ajax(JSON) (org.eclipse.jetty:jetty-util-ajax:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-util-ajax)
* Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-webapp)
+ * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-xml)
* Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-xml)
+ * Jetty :: ALPN :: API (org.eclipse.jetty.alpn:alpn-api:1.1.3.v20160715 - http://www.eclipse.org/jetty/alpn-api)
* Jetty :: HTTP2 :: Client (org.eclipse.jetty.http2:http2-client:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-client)
* Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:9.4.54.v20240208 - https://eclipse.org/jetty/http2-parent/http2-common)
* Jetty :: HTTP2 :: HPACK (org.eclipse.jetty.http2:http2-hpack:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-hpack)
* Jetty :: HTTP2 :: HTTP Client Transport (org.eclipse.jetty.http2:http2-http-client-transport:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-http-client-transport)
- * Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:9.4.54.v20240208 - https://eclipse.org/jetty/http2-parent/http2-server)
+ * Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:9.4.15.v20190215 - https://eclipse.org/jetty/http2-parent/http2-server)
+ * Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-server)
* Jetty :: Schemas (org.eclipse.jetty.toolchain:jetty-schemas:3.1.2 - https://eclipse.org/jetty/jetty-schemas)
* Ehcache (org.ehcache:ehcache:3.10.8 - http://ehcache.org)
- * flyway-core (org.flywaydb:flyway-core:8.5.13 - https://flywaydb.org/flyway-core)
+ * flyway-core (org.flywaydb:flyway-core:10.10.0 - https://flywaydb.org/flyway-core)
+ * flyway-database-postgresql (org.flywaydb:flyway-database-postgresql:10.10.0 - https://flywaydb.org/flyway-database-postgresql)
* Ogg and Vorbis for Java, Core (org.gagravarr:vorbis-java-core:0.8 - https://github.com/Gagravarr/VorbisJava)
* Apache Tika plugin for Ogg, Vorbis and FLAC (org.gagravarr:vorbis-java-tika:0.8 - https://github.com/Gagravarr/VorbisJava)
- * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.39.1 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client)
- * jersey-core-common (org.glassfish.jersey.core:jersey-common:2.39.1 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common)
- * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.39.1 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2)
- * Hibernate Validator Engine (org.hibernate.validator:hibernate-validator:6.2.5.Final - http://hibernate.org/validator/hibernate-validator)
- * Hibernate Validator Portable Extension (org.hibernate.validator:hibernate-validator-cdi:6.2.5.Final - http://hibernate.org/validator/hibernate-validator-cdi)
+ * jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common)
+ * Hibernate Validator Engine (org.hibernate.validator:hibernate-validator:8.0.1.Final - http://hibernate.org/validator/hibernate-validator)
+ * Hibernate Validator Portable Extension (org.hibernate.validator:hibernate-validator-cdi:8.0.1.Final - http://hibernate.org/validator/hibernate-validator-cdi)
* org.immutables.value-annotations (org.immutables:value-annotations:2.9.2 - http://immutables.org/value-annotations)
* leveldb (org.iq80.leveldb:leveldb:0.12 - http://github.com/dain/leveldb/leveldb)
* leveldb-api (org.iq80.leveldb:leveldb-api:0.12 - http://github.com/dain/leveldb/leveldb-api)
- * Javassist (org.javassist:javassist:3.29.0-GA - http://www.javassist.org/)
- * Java Annotation Indexer (org.jboss:jandex:2.4.2.Final - http://www.jboss.org/jandex)
+ * Javassist (org.javassist:javassist:3.29.2-GA - http://www.javassist.org/)
* JBoss Logging 3 (org.jboss.logging:jboss-logging:3.4.3.Final - http://www.jboss.org)
* JDOM (org.jdom:jdom2:2.0.6.1 - http://www.jdom.org)
* jtwig-core (org.jtwig:jtwig-core:5.87.0.RELEASE - http://jtwig.org)
@@ -351,8 +386,6 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
* MockServer Core (org.mock-server:mockserver-core:5.11.2 - http://www.mock-server.com)
* MockServer JUnit 4 Integration (org.mock-server:mockserver-junit-rule:5.11.2 - http://www.mock-server.com)
* MockServer & Proxy Netty (org.mock-server:mockserver-netty:5.11.2 - http://www.mock-server.com)
- * MortBay :: Apache EL :: API and Implementation (org.mortbay.jasper:apache-el:8.5.35.1 - https://github.com/jetty-project/jasper-jsp/apache-el)
- * MortBay :: Apache Jasper :: JSP Implementation (org.mortbay.jasper:apache-jsp:8.5.35.1 - https://github.com/jetty-project/jasper-jsp/apache-jsp)
* Jetty Server (org.mortbay.jetty:jetty:6.1.26 - http://www.eclipse.org/jetty/jetty-parent/project/modules/jetty)
* Jetty Servlet Tester (org.mortbay.jetty:jetty-servlet-tester:6.1.26 - http://www.eclipse.org/jetty/jetty-parent/project/jetty-servlet-tester)
* Jetty Utilities (org.mortbay.jetty:jetty-util:6.1.26 - http://www.eclipse.org/jetty/jetty-parent/project/jetty-util)
@@ -361,6 +394,8 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
* Objenesis (org.objenesis:objenesis:3.2 - http://objenesis.org/objenesis)
* parboiled-core (org.parboiled:parboiled-core:1.3.1 - http://parboiled.org)
* parboiled-java (org.parboiled:parboiled-java:1.3.1 - http://parboiled.org)
+ * org.roaringbitmap:RoaringBitmap (org.roaringbitmap:RoaringBitmap:0.9.45 - https://github.com/RoaringBitmap/RoaringBitmap)
+ * org.roaringbitmap:shims (org.roaringbitmap:shims:0.9.45 - https://github.com/RoaringBitmap/RoaringBitmap)
* RRD4J (org.rrd4j:rrd4j:3.5 - https://github.com/rrd4j/rrd4j/)
* Scala Library (org.scala-lang:scala-library:2.13.11 - https://www.scala-lang.org/)
* Scala Compiler (org.scala-lang:scala-reflect:2.13.0 - https://www.scala-lang.org/)
@@ -369,86 +404,82 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
* scala-parser-combinators (org.scala-lang.modules:scala-parser-combinators_2.13:1.1.2 - http://www.scala-lang.org/)
* scala-xml (org.scala-lang.modules:scala-xml_2.13:1.3.0 - http://www.scala-lang.org/)
* JSONassert (org.skyscreamer:jsonassert:1.5.1 - https://github.com/skyscreamer/JSONassert)
- * JCL 1.2 implemented over SLF4J (org.slf4j:jcl-over-slf4j:1.7.36 - http://www.slf4j.org)
- * Spring AOP (org.springframework:spring-aop:5.3.34 - https://github.com/spring-projects/spring-framework)
- * Spring Beans (org.springframework:spring-beans:5.3.34 - https://github.com/spring-projects/spring-framework)
- * Spring Context (org.springframework:spring-context:5.3.34 - https://github.com/spring-projects/spring-framework)
- * Spring Context Support (org.springframework:spring-context-support:5.3.34 - https://github.com/spring-projects/spring-framework)
- * Spring Core (org.springframework:spring-core:5.3.34 - https://github.com/spring-projects/spring-framework)
- * Spring Expression Language (SpEL) (org.springframework:spring-expression:5.3.34 - https://github.com/spring-projects/spring-framework)
- * Spring Commons Logging Bridge (org.springframework:spring-jcl:5.3.34 - https://github.com/spring-projects/spring-framework)
- * Spring JDBC (org.springframework:spring-jdbc:5.3.34 - https://github.com/spring-projects/spring-framework)
- * Spring Object/Relational Mapping (org.springframework:spring-orm:5.3.34 - https://github.com/spring-projects/spring-framework)
- * Spring TestContext Framework (org.springframework:spring-test:5.3.34 - https://github.com/spring-projects/spring-framework)
- * Spring Transaction (org.springframework:spring-tx:5.3.34 - https://github.com/spring-projects/spring-framework)
- * Spring Web (org.springframework:spring-web:5.3.34 - https://github.com/spring-projects/spring-framework)
- * Spring Web MVC (org.springframework:spring-webmvc:5.3.34 - https://github.com/spring-projects/spring-framework)
- * spring-boot (org.springframework.boot:spring-boot:2.7.18 - https://spring.io/projects/spring-boot)
- * spring-boot-actuator (org.springframework.boot:spring-boot-actuator:2.7.18 - https://spring.io/projects/spring-boot)
- * spring-boot-actuator-autoconfigure (org.springframework.boot:spring-boot-actuator-autoconfigure:2.7.18 - https://spring.io/projects/spring-boot)
- * spring-boot-autoconfigure (org.springframework.boot:spring-boot-autoconfigure:2.7.18 - https://spring.io/projects/spring-boot)
+ * JCL 1.2 implemented over SLF4J (org.slf4j:jcl-over-slf4j:2.0.11 - http://www.slf4j.org)
+ * Spring AOP (org.springframework:spring-aop:6.1.8 - https://github.com/spring-projects/spring-framework)
+ * Spring Beans (org.springframework:spring-beans:6.1.8 - https://github.com/spring-projects/spring-framework)
+ * Spring Context (org.springframework:spring-context:6.1.8 - https://github.com/spring-projects/spring-framework)
+ * Spring Context Support (org.springframework:spring-context-support:6.1.8 - https://github.com/spring-projects/spring-framework)
+ * Spring Core (org.springframework:spring-core:6.1.8 - https://github.com/spring-projects/spring-framework)
+ * Spring Expression Language (SpEL) (org.springframework:spring-expression:6.1.8 - https://github.com/spring-projects/spring-framework)
+ * Spring Commons Logging Bridge (org.springframework:spring-jcl:6.1.8 - https://github.com/spring-projects/spring-framework)
+ * Spring JDBC (org.springframework:spring-jdbc:6.1.8 - https://github.com/spring-projects/spring-framework)
+ * Spring Object/Relational Mapping (org.springframework:spring-orm:6.1.8 - https://github.com/spring-projects/spring-framework)
+ * Spring TestContext Framework (org.springframework:spring-test:6.1.8 - https://github.com/spring-projects/spring-framework)
+ * Spring Transaction (org.springframework:spring-tx:6.1.8 - https://github.com/spring-projects/spring-framework)
+ * Spring Web (org.springframework:spring-web:6.1.8 - https://github.com/spring-projects/spring-framework)
+ * Spring Web MVC (org.springframework:spring-webmvc:6.1.8 - https://github.com/spring-projects/spring-framework)
+ * spring-boot (org.springframework.boot:spring-boot:3.2.6 - https://spring.io/projects/spring-boot)
+ * spring-boot-actuator (org.springframework.boot:spring-boot-actuator:3.2.6 - https://spring.io/projects/spring-boot)
+ * spring-boot-actuator-autoconfigure (org.springframework.boot:spring-boot-actuator-autoconfigure:3.2.6 - https://spring.io/projects/spring-boot)
+ * spring-boot-autoconfigure (org.springframework.boot:spring-boot-autoconfigure:3.2.6 - https://spring.io/projects/spring-boot)
* Spring Boot Configuration Processor (org.springframework.boot:spring-boot-configuration-processor:2.0.0.RELEASE - https://projects.spring.io/spring-boot/#/spring-boot-parent/spring-boot-tools/spring-boot-configuration-processor)
- * spring-boot-starter (org.springframework.boot:spring-boot-starter:2.7.18 - https://spring.io/projects/spring-boot)
- * spring-boot-starter-actuator (org.springframework.boot:spring-boot-starter-actuator:2.7.18 - https://spring.io/projects/spring-boot)
- * spring-boot-starter-aop (org.springframework.boot:spring-boot-starter-aop:2.7.18 - https://spring.io/projects/spring-boot)
- * spring-boot-starter-cache (org.springframework.boot:spring-boot-starter-cache:2.7.18 - https://spring.io/projects/spring-boot)
- * spring-boot-starter-data-rest (org.springframework.boot:spring-boot-starter-data-rest:2.7.18 - https://spring.io/projects/spring-boot)
- * spring-boot-starter-json (org.springframework.boot:spring-boot-starter-json:2.7.18 - https://spring.io/projects/spring-boot)
- * spring-boot-starter-log4j2 (org.springframework.boot:spring-boot-starter-log4j2:2.7.18 - https://spring.io/projects/spring-boot)
- * spring-boot-starter-security (org.springframework.boot:spring-boot-starter-security:2.7.18 - https://spring.io/projects/spring-boot)
- * spring-boot-starter-test (org.springframework.boot:spring-boot-starter-test:2.7.18 - https://spring.io/projects/spring-boot)
- * spring-boot-starter-tomcat (org.springframework.boot:spring-boot-starter-tomcat:2.7.18 - https://spring.io/projects/spring-boot)
- * spring-boot-starter-web (org.springframework.boot:spring-boot-starter-web:2.7.18 - https://spring.io/projects/spring-boot)
- * spring-boot-test (org.springframework.boot:spring-boot-test:2.7.18 - https://spring.io/projects/spring-boot)
- * spring-boot-test-autoconfigure (org.springframework.boot:spring-boot-test-autoconfigure:2.7.18 - https://spring.io/projects/spring-boot)
- * Spring Data Core (org.springframework.data:spring-data-commons:2.7.18 - https://www.spring.io/spring-data/spring-data-commons)
- * Spring Data REST - Core (org.springframework.data:spring-data-rest-core:3.7.18 - https://www.spring.io/spring-data/spring-data-rest-parent/spring-data-rest-core)
- * Spring Data REST - WebMVC (org.springframework.data:spring-data-rest-webmvc:3.7.18 - https://www.spring.io/spring-data/spring-data-rest-parent/spring-data-rest-webmvc)
- * Spring HATEOAS (org.springframework.hateoas:spring-hateoas:1.5.6 - https://github.com/spring-projects/spring-hateoas)
- * Spring Plugin - Core (org.springframework.plugin:spring-plugin-core:2.0.0.RELEASE - https://github.com/spring-projects/spring-plugin/spring-plugin-core)
- * spring-security-config (org.springframework.security:spring-security-config:5.7.11 - https://spring.io/projects/spring-security)
- * spring-security-core (org.springframework.security:spring-security-core:5.7.11 - https://spring.io/projects/spring-security)
- * spring-security-crypto (org.springframework.security:spring-security-crypto:5.7.11 - https://spring.io/projects/spring-security)
- * spring-security-test (org.springframework.security:spring-security-test:5.7.11 - https://spring.io/projects/spring-security)
- * spring-security-web (org.springframework.security:spring-security-web:5.7.11 - https://spring.io/projects/spring-security)
- * SWORD v2 :: Common Server Library (org.swordapp:sword2-server:1.0 - http://www.swordapp.org/)
+ * spring-boot-starter (org.springframework.boot:spring-boot-starter:3.2.6 - https://spring.io/projects/spring-boot)
+ * spring-boot-starter-actuator (org.springframework.boot:spring-boot-starter-actuator:3.2.6 - https://spring.io/projects/spring-boot)
+ * spring-boot-starter-aop (org.springframework.boot:spring-boot-starter-aop:3.2.6 - https://spring.io/projects/spring-boot)
+ * spring-boot-starter-cache (org.springframework.boot:spring-boot-starter-cache:3.2.6 - https://spring.io/projects/spring-boot)
+ * spring-boot-starter-data-rest (org.springframework.boot:spring-boot-starter-data-rest:3.2.6 - https://spring.io/projects/spring-boot)
+ * spring-boot-starter-json (org.springframework.boot:spring-boot-starter-json:3.2.6 - https://spring.io/projects/spring-boot)
+ * spring-boot-starter-log4j2 (org.springframework.boot:spring-boot-starter-log4j2:3.2.6 - https://spring.io/projects/spring-boot)
+ * spring-boot-starter-security (org.springframework.boot:spring-boot-starter-security:3.2.6 - https://spring.io/projects/spring-boot)
+ * spring-boot-starter-test (org.springframework.boot:spring-boot-starter-test:3.2.6 - https://spring.io/projects/spring-boot)
+ * spring-boot-starter-tomcat (org.springframework.boot:spring-boot-starter-tomcat:3.2.6 - https://spring.io/projects/spring-boot)
+ * spring-boot-starter-web (org.springframework.boot:spring-boot-starter-web:3.2.6 - https://spring.io/projects/spring-boot)
+ * spring-boot-test (org.springframework.boot:spring-boot-test:3.2.6 - https://spring.io/projects/spring-boot)
+ * spring-boot-test-autoconfigure (org.springframework.boot:spring-boot-test-autoconfigure:3.2.6 - https://spring.io/projects/spring-boot)
+ * Spring Data Core (org.springframework.data:spring-data-commons:3.2.6 - https://spring.io/projects/spring-data)
+ * Spring Data REST - Core (org.springframework.data:spring-data-rest-core:4.2.6 - https://www.spring.io/spring-data/spring-data-rest-parent/spring-data-rest-core)
+ * Spring Data REST - WebMVC (org.springframework.data:spring-data-rest-webmvc:4.2.6 - https://www.spring.io/spring-data/spring-data-rest-parent/spring-data-rest-webmvc)
+ * Spring HATEOAS (org.springframework.hateoas:spring-hateoas:2.2.2 - https://github.com/spring-projects/spring-hateoas)
+ * Spring Plugin - Core (org.springframework.plugin:spring-plugin-core:3.0.0 - https://github.com/spring-projects/spring-plugin/spring-plugin-core)
+ * spring-security-config (org.springframework.security:spring-security-config:6.2.4 - https://spring.io/projects/spring-security)
+ * spring-security-core (org.springframework.security:spring-security-core:6.2.4 - https://spring.io/projects/spring-security)
+ * spring-security-crypto (org.springframework.security:spring-security-crypto:6.2.4 - https://spring.io/projects/spring-security)
+ * spring-security-test (org.springframework.security:spring-security-test:6.2.4 - https://spring.io/projects/spring-security)
+ * spring-security-web (org.springframework.security:spring-security-web:6.2.4 - https://spring.io/projects/spring-security)
* snappy-java (org.xerial.snappy:snappy-java:1.1.10.1 - https://github.com/xerial/snappy-java)
* xml-matchers (org.xmlmatchers:xml-matchers:0.10 - http://code.google.com/p/xml-matchers/)
* org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.10.0 - https://www.xmlunit.org/)
* org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.9.1 - https://www.xmlunit.org/)
* org.xmlunit:xmlunit-placeholders (org.xmlunit:xmlunit-placeholders:2.8.0 - https://www.xmlunit.org/xmlunit-placeholders/)
- * SnakeYAML (org.yaml:snakeyaml:1.30 - https://bitbucket.org/snakeyaml/snakeyaml)
+ * SnakeYAML (org.yaml:snakeyaml:2.2 - https://bitbucket.org/snakeyaml/snakeyaml)
* software.amazon.ion:ion-java (software.amazon.ion:ion-java:1.0.2 - https://github.com/amznlabs/ion-java/)
* Xerces2-j (xerces:xercesImpl:2.12.2 - https://xerces.apache.org/xerces2-j/)
- * XML Commons External Components XML APIs (xml-apis:xml-apis:1.4.01 - http://xml.apache.org/commons/components/external/)
BSD License:
- * AntLR Parser Generator (antlr:antlr:2.7.7 - http://www.antlr.org/)
* Adobe XMPCore (com.adobe.xmp:xmpcore:6.1.11 - https://www.adobe.com/devnet/xmp/library/eula-xmp-library-java.html)
* coverity-escapers (com.coverity.security:coverity-escapers:1.1.1 - http://coverity.com/security)
* Java Advanced Imaging Image I/O Tools API core (standalone) (com.github.jai-imageio:jai-imageio-core:1.4.0 - https://github.com/jai-imageio/jai-imageio-core)
- * JSONLD Java :: Core (com.github.jsonld-java:jsonld-java:0.5.1 - http://github.com/jsonld-java/jsonld-java/jsonld-java/)
+ * JSONLD Java :: Core (com.github.jsonld-java:jsonld-java:0.13.4 - http://github.com/jsonld-java/jsonld-java/jsonld-java/)
* curvesapi (com.github.virtuald:curvesapi:1.08 - https://github.com/virtuald/curvesapi)
* Protocol Buffers [Core] (com.google.protobuf:protobuf-java:3.15.0 - https://developers.google.com/protocol-buffers/protobuf-java/)
+ * Protocol Buffers [Core] (com.google.protobuf:protobuf-java:3.23.3 - https://developers.google.com/protocol-buffers/protobuf-java/)
* JZlib (com.jcraft:jzlib:1.1.3 - http://www.jcraft.com/jzlib/)
* dnsjava (dnsjava:dnsjava:2.1.9 - http://www.dnsjava.org)
* jaxen (jaxen:jaxen:2.0.0 - http://www.cafeconleche.org/jaxen/jaxen)
- * ANTLR 4 Runtime (org.antlr:antlr4-runtime:4.5.1-1 - http://www.antlr.org/antlr4-runtime)
+ * ANTLR 4 Runtime (org.antlr:antlr4-runtime:4.13.1 - https://www.antlr.org/antlr4-runtime/)
* commons-compiler (org.codehaus.janino:commons-compiler:3.1.8 - http://janino-compiler.github.io/commons-compiler/)
* janino (org.codehaus.janino:janino:3.1.8 - http://janino-compiler.github.io/janino/)
* Stax2 API (org.codehaus.woodstox:stax2-api:4.2.1 - http://github.com/FasterXML/stax2-api)
* Hamcrest Date (org.exparity:hamcrest-date:2.0.8 - https://github.com/exparity/hamcrest-date)
- * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.39.1 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client)
- * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.39.1 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2)
* Hamcrest (org.hamcrest:hamcrest:2.2 - http://hamcrest.org/JavaHamcrest/)
* Hamcrest Core (org.hamcrest:hamcrest-core:2.2 - http://hamcrest.org/JavaHamcrest/)
* HdrHistogram (org.hdrhistogram:HdrHistogram:2.1.12 - http://hdrhistogram.github.io/HdrHistogram/)
* JBibTeX (org.jbibtex:jbibtex:1.0.20 - http://www.jbibtex.org)
* asm (org.ow2.asm:asm:8.0.1 - http://asm.ow2.io/)
- * asm-analysis (org.ow2.asm:asm-analysis:7.1 - http://asm.ow2.org/)
- * asm-commons (org.ow2.asm:asm-commons:9.3 - http://asm.ow2.io/)
- * asm-tree (org.ow2.asm:asm-tree:7.1 - http://asm.ow2.org/)
+ * asm-analysis (org.ow2.asm:asm-analysis:8.0.1 - http://asm.ow2.io/)
+ * asm-commons (org.ow2.asm:asm-commons:8.0.1 - http://asm.ow2.io/)
+ * asm-tree (org.ow2.asm:asm-tree:8.0.1 - http://asm.ow2.io/)
* asm-util (org.ow2.asm:asm-util:7.1 - http://asm.ow2.org/)
* PostgreSQL JDBC Driver (org.postgresql:postgresql:42.7.3 - https://jdbc.postgresql.org)
* Reflections (org.reflections:reflections:0.9.12 - http://github.com/ronmamo/reflections)
@@ -461,48 +492,54 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
Common Development and Distribution License (CDDL):
- * JavaMail API (com.sun.mail:javax.mail:1.6.2 - http://javaee.github.io/javamail/javax.mail)
* JavaMail API (no providers) (com.sun.mail:mailapi:1.6.2 - http://javaee.github.io/javamail/mailapi)
* Old JAXB Core (com.sun.xml.bind:jaxb-core:2.3.0.1 - http://jaxb.java.net/jaxb-bundles/jaxb-core)
* Old JAXB Runtime (com.sun.xml.bind:jaxb-impl:2.3.1 - http://jaxb.java.net/jaxb-bundles/jaxb-impl)
- * Jakarta Annotations API (jakarta.annotation:jakarta.annotation-api:1.3.5 - https://projects.eclipse.org/projects/ee4j.ca)
- * javax.transaction API (jakarta.transaction:jakarta.transaction-api:1.3.3 - https://projects.eclipse.org/projects/ee4j.jta)
- * jakarta.ws.rs-api (jakarta.ws.rs:jakarta.ws.rs-api:2.1.6 - https://github.com/eclipse-ee4j/jaxrs-api)
- * JavaBeans Activation Framework (JAF) (javax.activation:activation:1.1 - http://java.sun.com/products/javabeans/jaf/index.jsp)
+ * Jakarta Annotations API (jakarta.annotation:jakarta.annotation-api:2.1.1 - https://projects.eclipse.org/projects/ee4j.ca)
+ * Jakarta Mail API (jakarta.mail:jakarta.mail-api:2.1.3 - https://projects.eclipse.org/projects/ee4j/jakarta.mail-api)
+ * Jakarta Servlet (jakarta.servlet:jakarta.servlet-api:6.0.0 - https://projects.eclipse.org/projects/ee4j.servlet)
+ * jakarta.transaction API (jakarta.transaction:jakarta.transaction-api:2.0.1 - https://projects.eclipse.org/projects/ee4j.jta)
* JavaBeans Activation Framework API jar (javax.activation:javax.activation-api:1.2.0 - http://java.net/all/javax.activation-api/)
- * javax.annotation API (javax.annotation:javax.annotation-api:1.3.2 - http://jcp.org/en/jsr/detail?id=250)
+ * javax.annotation API (javax.annotation:javax.annotation-api:1.3 - http://jcp.org/en/jsr/detail?id=250)
* Java Servlet API (javax.servlet:javax.servlet-api:3.1.0 - http://servlet-spec.java.net)
* javax.transaction API (javax.transaction:javax.transaction-api:1.3 - http://jta-spec.java.net)
* jaxb-api (javax.xml.bind:jaxb-api:2.3.1 - https://github.com/javaee/jaxb-spec/jaxb-api)
* JHighlight (org.codelibs:jhighlight:1.1.0 - https://github.com/codelibs/jhighlight)
- * HK2 API module (org.glassfish.hk2:hk2-api:2.6.1 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-api)
- * ServiceLocator Default Implementation (org.glassfish.hk2:hk2-locator:2.6.1 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-locator)
- * HK2 Implementation Utilities (org.glassfish.hk2:hk2-utils:2.6.1 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-utils)
+ * Angus Mail default provider (org.eclipse.angus:jakarta.mail:2.0.3 - http://eclipse-ee4j.github.io/angus-mail/jakarta.mail)
+ * HK2 API module (org.glassfish.hk2:hk2-api:3.0.5 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-api)
+ * ServiceLocator Default Implementation (org.glassfish.hk2:hk2-locator:3.0.5 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-locator)
+ * HK2 Implementation Utilities (org.glassfish.hk2:hk2-utils:3.0.5 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-utils)
* OSGi resource locator (org.glassfish.hk2:osgi-resource-locator:1.0.3 - https://projects.eclipse.org/projects/ee4j/osgi-resource-locator)
- * aopalliance version 1.0 repackaged as a module (org.glassfish.hk2.external:aopalliance-repackaged:2.6.1 - https://github.com/eclipse-ee4j/glassfish-hk2/external/aopalliance-repackaged)
- * javax.inject:1 as OSGi bundle (org.glassfish.hk2.external:jakarta.inject:2.6.1 - https://github.com/eclipse-ee4j/glassfish-hk2/external/jakarta.inject)
- * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.39.1 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client)
- * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.39.1 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2)
- * Java Transaction API (org.jboss.spec.javax.transaction:jboss-transaction-api_1.2_spec:1.1.1.Final - http://www.jboss.org/jboss-transaction-api_1.2_spec)
+ * aopalliance version 1.0 repackaged as a module (org.glassfish.hk2.external:aopalliance-repackaged:3.0.5 - https://github.com/eclipse-ee4j/glassfish-hk2/external/aopalliance-repackaged)
+ * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client)
+ * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2)
+ * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart)
Cordra (Version 2) License Agreement:
- * net.cnri:cnri-servlet-container (net.cnri:cnri-servlet-container:3.0.0 - https://gitlab.com/cnri/cnri-servlet-container)
- * net.cnri:cnri-servlet-container-lib (net.cnri:cnri-servlet-container-lib:3.0.0 - https://gitlab.com/cnri/cnri-servlet-container)
+ * net.cnri:cnri-servlet-container-lib (net.cnri:cnri-servlet-container-lib:3.1.0 - https://gitlab.com/cnri/cnri-servlet-container)
* net.cnri:cnriutil (net.cnri:cnriutil:2.0 - https://gitlab.com/cnri/cnriutil)
+ Cordra (Version 2.5.0) License Agreement:
+
+ * net.cnri:cnri-servlet-container (net.cnri:cnri-servlet-container:3.1.0 - https://gitlab.com/cnri/cnri-servlet-container)
+
Eclipse Distribution License, Version 1.0:
- * Jakarta Activation (com.sun.activation:jakarta.activation:1.2.2 - https://github.com/eclipse-ee4j/jaf/jakarta.activation)
- * istack common utility code runtime (com.sun.istack:istack-commons-runtime:3.0.12 - https://projects.eclipse.org/projects/ee4j/istack-commons/istack-commons-runtime)
- * Jakarta Activation API jar (jakarta.activation:jakarta.activation-api:1.2.2 - https://github.com/eclipse-ee4j/jaf/jakarta.activation-api)
- * Jakarta XML Binding API (jakarta.xml.bind:jakarta.xml.bind-api:2.3.3 - https://github.com/eclipse-ee4j/jaxb-api/jakarta.xml.bind-api)
- * javax.persistence-api (javax.persistence:javax.persistence-api:2.2 - https://github.com/javaee/jpa-spec)
- * JAXB Runtime (org.glassfish.jaxb:jaxb-runtime:2.3.9 - https://eclipse-ee4j.github.io/jaxb-ri/)
- * TXW2 Runtime (org.glassfish.jaxb:txw2:2.3.9 - https://eclipse-ee4j.github.io/jaxb-ri/)
- * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.39.1 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client)
- * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.39.1 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2)
- * Java Persistence API, Version 2.1 (org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final - http://hibernate.org)
+ * istack common utility code runtime (com.sun.istack:istack-commons-runtime:4.1.2 - https://projects.eclipse.org/projects/ee4j/istack-commons/istack-commons-runtime)
+ * Jakarta Activation API (jakarta.activation:jakarta.activation-api:2.1.3 - https://github.com/jakartaee/jaf-api)
+ * Jakarta Mail API (jakarta.mail:jakarta.mail-api:2.1.3 - https://projects.eclipse.org/projects/ee4j/jakarta.mail-api)
+ * Jakarta Persistence API (jakarta.persistence:jakarta.persistence-api:3.1.0 - https://github.com/eclipse-ee4j/jpa-api)
+ * Jakarta XML Binding API (jakarta.xml.bind:jakarta.xml.bind-api:4.0.2 - https://github.com/jakartaee/jaxb-api/jakarta.xml.bind-api)
+ * Angus Activation Registries (org.eclipse.angus:angus-activation:2.0.2 - https://github.com/eclipse-ee4j/angus-activation/angus-activation)
+ * Angus Mail default provider (org.eclipse.angus:jakarta.mail:2.0.3 - http://eclipse-ee4j.github.io/angus-mail/jakarta.mail)
+ * JAXB Core (org.glassfish.jaxb:jaxb-core:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/)
+ * JAXB Runtime (org.glassfish.jaxb:jaxb-runtime:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/)
+ * TXW2 Runtime (org.glassfish.jaxb:txw2:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/)
+ * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client)
+ * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2)
+ * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart)
+ * MIME streaming extension (org.jvnet.mimepull:mimepull:1.9.15 - https://github.com/eclipse-ee4j/metro-mimepull)
* org.locationtech.jts:jts-core (org.locationtech.jts:jts-core:1.19.0 - https://www.locationtech.org/projects/technology.jts/jts-modules/jts-core)
* org.locationtech.jts.io:jts-io-common (org.locationtech.jts.io:jts-io-common:1.19.0 - https://www.locationtech.org/projects/technology.jts/jts-modules/jts-io/jts-io-common)
@@ -510,24 +547,28 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
* System Rules (com.github.stefanbirkner:system-rules:1.19.0 - http://stefanbirkner.github.io/system-rules/)
* H2 Database Engine (com.h2database:h2:2.2.224 - https://h2database.com)
- * Jakarta Annotations API (jakarta.annotation:jakarta.annotation-api:1.3.5 - https://projects.eclipse.org/projects/ee4j.ca)
- * javax.transaction API (jakarta.transaction:jakarta.transaction-api:1.3.3 - https://projects.eclipse.org/projects/ee4j.jta)
- * jakarta.ws.rs-api (jakarta.ws.rs:jakarta.ws.rs-api:2.1.6 - https://github.com/eclipse-ee4j/jaxrs-api)
- * javax.persistence-api (javax.persistence:javax.persistence-api:2.2 - https://github.com/javaee/jpa-spec)
+ * Jakarta Annotations API (jakarta.annotation:jakarta.annotation-api:2.1.1 - https://projects.eclipse.org/projects/ee4j.ca)
+ * Jakarta Expression Language API (jakarta.el:jakarta.el-api:5.0.1 - https://projects.eclipse.org/projects/ee4j.el)
+ * Jakarta Mail API (jakarta.mail:jakarta.mail-api:2.1.3 - https://projects.eclipse.org/projects/ee4j/jakarta.mail-api)
+ * Jakarta Persistence API (jakarta.persistence:jakarta.persistence-api:3.1.0 - https://github.com/eclipse-ee4j/jpa-api)
+ * Jakarta Servlet (jakarta.servlet:jakarta.servlet-api:6.0.0 - https://projects.eclipse.org/projects/ee4j.servlet)
+ * jakarta.transaction API (jakarta.transaction:jakarta.transaction-api:2.0.1 - https://projects.eclipse.org/projects/ee4j.jta)
+ * Jakarta RESTful WS API (jakarta.ws.rs:jakarta.ws.rs-api:3.1.0 - https://github.com/eclipse-ee4j/jaxrs-api)
* JUnit (junit:junit:4.13.2 - http://junit.org)
- * AspectJ Weaver (org.aspectj:aspectjweaver:1.9.7 - https://www.eclipse.org/aspectj/)
- * Eclipse Compiler for Java(TM) (org.eclipse.jdt:ecj:3.14.0 - http://www.eclipse.org/jdt)
+ * AspectJ Weaver (org.aspectj:aspectjweaver:1.9.22 - https://www.eclipse.org/aspectj/)
+ * Angus Mail default provider (org.eclipse.angus:jakarta.mail:2.0.3 - http://eclipse-ee4j.github.io/angus-mail/jakarta.mail)
* Jetty :: Apache JSP Implementation (org.eclipse.jetty:apache-jsp:9.4.15.v20190215 - http://www.eclipse.org/jetty)
* Apache :: JSTL module (org.eclipse.jetty:apache-jstl:9.4.15.v20190215 - http://tomcat.apache.org/taglibs/standard/)
* Jetty :: ALPN :: Client (org.eclipse.jetty:jetty-alpn-client:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-client)
* Jetty :: ALPN :: JDK9 Client Implementation (org.eclipse.jetty:jetty-alpn-java-client:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-java-client)
- * Jetty :: ALPN :: JDK9 Server Implementation (org.eclipse.jetty:jetty-alpn-java-server:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-java-server)
+ * Jetty :: ALPN :: JDK9 Server Implementation (org.eclipse.jetty:jetty-alpn-java-server:9.4.15.v20190215 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-java-server)
+ * Jetty :: ALPN :: JDK9 Server Implementation (org.eclipse.jetty:jetty-alpn-java-server:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-java-server)
+ * Jetty :: ALPN :: Server (org.eclipse.jetty:jetty-alpn-server:9.4.15.v20190215 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-server)
* Jetty :: ALPN :: Server (org.eclipse.jetty:jetty-alpn-server:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-server)
- * Jetty :: ALPN :: Server (org.eclipse.jetty:jetty-alpn-server:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-server)
* Jetty :: Servlet Annotations (org.eclipse.jetty:jetty-annotations:9.4.15.v20190215 - http://www.eclipse.org/jetty)
* Jetty :: Asynchronous HTTP Client (org.eclipse.jetty:jetty-client:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-client)
+ * Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.15.v20190215 - http://www.eclipse.org/jetty)
* Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-continuation)
- * Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-continuation)
* Jetty :: Deployers (org.eclipse.jetty:jetty-deploy:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-deploy)
* Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-http)
* Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-io)
@@ -539,41 +580,37 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
* Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-security)
* Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-server)
* Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-servlet)
- * Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-servlets)
+ * Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.4.15.v20190215 - http://www.eclipse.org/jetty)
+ * Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-servlets)
* Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-util)
* Jetty :: Utilities :: Ajax(JSON) (org.eclipse.jetty:jetty-util-ajax:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-util-ajax)
* Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-webapp)
+ * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-xml)
* Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-xml)
+ * Jetty :: ALPN :: API (org.eclipse.jetty.alpn:alpn-api:1.1.3.v20160715 - http://www.eclipse.org/jetty/alpn-api)
* Jetty :: HTTP2 :: Client (org.eclipse.jetty.http2:http2-client:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-client)
* Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:9.4.54.v20240208 - https://eclipse.org/jetty/http2-parent/http2-common)
* Jetty :: HTTP2 :: HPACK (org.eclipse.jetty.http2:http2-hpack:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-hpack)
* Jetty :: HTTP2 :: HTTP Client Transport (org.eclipse.jetty.http2:http2-http-client-transport:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-http-client-transport)
- * Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:9.4.54.v20240208 - https://eclipse.org/jetty/http2-parent/http2-server)
+ * Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:9.4.15.v20190215 - https://eclipse.org/jetty/http2-parent/http2-server)
+ * Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-server)
* Jetty :: Schemas (org.eclipse.jetty.toolchain:jetty-schemas:3.1.2 - https://eclipse.org/jetty/jetty-schemas)
- * HK2 API module (org.glassfish.hk2:hk2-api:2.6.1 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-api)
- * ServiceLocator Default Implementation (org.glassfish.hk2:hk2-locator:2.6.1 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-locator)
- * HK2 Implementation Utilities (org.glassfish.hk2:hk2-utils:2.6.1 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-utils)
+ * JSON-P Default Provider (org.glassfish:jakarta.json:2.0.1 - https://github.com/eclipse-ee4j/jsonp)
+ * HK2 API module (org.glassfish.hk2:hk2-api:3.0.5 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-api)
+ * ServiceLocator Default Implementation (org.glassfish.hk2:hk2-locator:3.0.5 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-locator)
+ * HK2 Implementation Utilities (org.glassfish.hk2:hk2-utils:3.0.5 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-utils)
* OSGi resource locator (org.glassfish.hk2:osgi-resource-locator:1.0.3 - https://projects.eclipse.org/projects/ee4j/osgi-resource-locator)
- * aopalliance version 1.0 repackaged as a module (org.glassfish.hk2.external:aopalliance-repackaged:2.6.1 - https://github.com/eclipse-ee4j/glassfish-hk2/external/aopalliance-repackaged)
- * javax.inject:1 as OSGi bundle (org.glassfish.hk2.external:jakarta.inject:2.6.1 - https://github.com/eclipse-ee4j/glassfish-hk2/external/jakarta.inject)
- * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.39.1 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client)
- * jersey-core-common (org.glassfish.jersey.core:jersey-common:2.39.1 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common)
- * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.39.1 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2)
- * Java Persistence API, Version 2.1 (org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final - http://hibernate.org)
+ * aopalliance version 1.0 repackaged as a module (org.glassfish.hk2.external:aopalliance-repackaged:3.0.5 - https://github.com/eclipse-ee4j/glassfish-hk2/external/aopalliance-repackaged)
+ * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client)
+ * jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common)
+ * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2)
+ * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart)
* org.locationtech.jts:jts-core (org.locationtech.jts:jts-core:1.19.0 - https://www.locationtech.org/projects/technology.jts/jts-modules/jts-core)
* org.locationtech.jts.io:jts-io-common (org.locationtech.jts.io:jts-io-common:1.19.0 - https://www.locationtech.org/projects/technology.jts/jts-modules/jts-io/jts-io-common)
* Jetty Server (org.mortbay.jetty:jetty:6.1.26 - http://www.eclipse.org/jetty/jetty-parent/project/modules/jetty)
* Jetty Servlet Tester (org.mortbay.jetty:jetty-servlet-tester:6.1.26 - http://www.eclipse.org/jetty/jetty-parent/project/jetty-servlet-tester)
* Jetty Utilities (org.mortbay.jetty:jetty-util:6.1.26 - http://www.eclipse.org/jetty/jetty-parent/project/jetty-util)
- GENERAL PUBLIC LICENSE, version 3 (GPL-3.0):
-
- * juniversalchardet (com.github.albfernandez:juniversalchardet:2.4.0 - https://github.com/albfernandez/juniversalchardet)
-
- GNU LESSER GENERAL PUBLIC LICENSE, version 3 (LGPL-3.0):
-
- * juniversalchardet (com.github.albfernandez:juniversalchardet:2.4.0 - https://github.com/albfernandez/juniversalchardet)
-
GNU Lesser General Public License (LGPL):
* btf (com.github.java-json-tools:btf:1.3 - https://github.com/java-json-tools/btf)
@@ -585,12 +622,12 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
* uri-template (com.github.java-json-tools:uri-template:0.10 - https://github.com/java-json-tools/uri-template)
* FindBugs-Annotations (com.google.code.findbugs:annotations:3.0.1u2 - http://findbugs.sourceforge.net/)
* JHighlight (org.codelibs:jhighlight:1.1.0 - https://github.com/codelibs/jhighlight)
- * Hibernate ORM - hibernate-core (org.hibernate:hibernate-core:5.6.15.Final - https://hibernate.org/orm)
- * Hibernate ORM - hibernate-jcache (org.hibernate:hibernate-jcache:5.6.15.Final - https://hibernate.org/orm)
- * Hibernate ORM - hibernate-jpamodelgen (org.hibernate:hibernate-jpamodelgen:5.6.15.Final - https://hibernate.org/orm)
- * Hibernate Commons Annotations (org.hibernate.common:hibernate-commons-annotations:5.1.2.Final - http://hibernate.org)
+ * Hibernate Commons Annotations (org.hibernate.common:hibernate-commons-annotations:6.0.6.Final - http://hibernate.org)
+ * Hibernate ORM - hibernate-core (org.hibernate.orm:hibernate-core:6.4.8.Final - https://hibernate.org/orm)
+ * Hibernate ORM - hibernate-jcache (org.hibernate.orm:hibernate-jcache:6.4.8.Final - https://hibernate.org/orm)
+ * Hibernate ORM - hibernate-jpamodelgen (org.hibernate.orm:hibernate-jpamodelgen:6.4.8.Final - https://hibernate.org/orm)
* im4java (org.im4java:im4java:1.4.0 - http://sourceforge.net/projects/im4java/)
- * Javassist (org.javassist:javassist:3.29.0-GA - http://www.javassist.org/)
+ * Javassist (org.javassist:javassist:3.29.2-GA - http://www.javassist.org/)
* XOM (xom:xom:1.3.9 - https://xom.nu)
Go License:
@@ -607,26 +644,28 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
MIT License:
+ * dexx (com.github.andrewoma.dexx:collection:0.7 - https://github.com/andrewoma/dexx)
* better-files (com.github.pathikrit:better-files_2.13:3.9.1 - https://github.com/pathikrit/better-files)
* Java SemVer (com.github.zafarkhaja:java-semver:0.9.0 - https://github.com/zafarkhaja/jsemver)
* dd-plist (com.googlecode.plist:dd-plist:1.28 - http://www.github.com/3breadt/dd-plist)
* DigitalCollections: IIIF API Library (de.digitalcollections.iiif:iiif-apis:0.3.10 - https://github.com/dbmdz/iiif-apis)
* s3mock (io.findify:s3mock_2.13:0.2.6 - https://github.com/findify/s3mock)
+ * ClassGraph (io.github.classgraph:classgraph:4.8.165 - https://github.com/classgraph/classgraph)
* JOpt Simple (net.sf.jopt-simple:jopt-simple:5.0.4 - http://jopt-simple.github.io/jopt-simple)
* Bouncy Castle S/MIME API (org.bouncycastle:bcmail-jdk18on:1.77 - https://www.bouncycastle.org/java.html)
+ * Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs (org.bouncycastle:bcpkix-jdk15on:1.67 - http://www.bouncycastle.org/java.html)
* Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs (org.bouncycastle:bcpkix-jdk18on:1.78.1 - https://www.bouncycastle.org/java.html)
+ * Bouncy Castle Provider (org.bouncycastle:bcprov-jdk15on:1.67 - http://www.bouncycastle.org/java.html)
* Bouncy Castle Provider (org.bouncycastle:bcprov-jdk18on:1.78.1 - https://www.bouncycastle.org/java.html)
* Bouncy Castle ASN.1 Extension and Utility APIs (org.bouncycastle:bcutil-jdk18on:1.78.1 - https://www.bouncycastle.org/java.html)
* org.brotli:dec (org.brotli:dec:0.1.2 - http://brotli.org/dec)
- * Checker Qual (org.checkerframework:checker-qual:3.23.0 - https://checkerframework.org)
- * Checker Qual (org.checkerframework:checker-qual:3.42.0 - https://checkerframework.org/)
- * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.39.1 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client)
- * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.39.1 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2)
+ * Checker Qual (org.checkerframework:checker-qual:3.31.0 - https://checkerframework.org)
+ * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client)
+ * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2)
+ * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart)
* mockito-core (org.mockito:mockito-core:3.12.4 - https://github.com/mockito/mockito)
* mockito-inline (org.mockito:mockito-inline:3.12.4 - https://github.com/mockito/mockito)
- * ORCID - Model (org.orcid:orcid-model:3.0.2 - http://github.com/ORCID/orcid-model)
- * JUL to SLF4J bridge (org.slf4j:jul-to-slf4j:1.7.36 - http://www.slf4j.org)
- * SLF4J API Module (org.slf4j:slf4j-api:1.7.36 - http://www.slf4j.org)
+ * SLF4J API Module (org.slf4j:slf4j-api:2.0.11 - http://www.slf4j.org)
* SLF4J Extensions Module (org.slf4j:slf4j-ext:1.7.28 - http://www.slf4j.org)
* HAL Browser (org.webjars:hal-browser:ad9b865 - http://webjars.org)
* toastr (org.webjars.bowergithub.codeseven:toastr:2.1.4 - http://webjars.org)
@@ -643,14 +682,15 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
* juniversalchardet (com.github.albfernandez:juniversalchardet:2.4.0 - https://github.com/albfernandez/juniversalchardet)
* H2 Database Engine (com.h2database:h2:2.2.224 - https://h2database.com)
* Saxon-HE (net.sf.saxon:Saxon-HE:9.8.0-14 - http://www.saxonica.com/)
- * Javassist (org.javassist:javassist:3.29.0-GA - http://www.javassist.org/)
+ * Javassist (org.javassist:javassist:3.29.2-GA - http://www.javassist.org/)
* Mozilla Rhino (org.mozilla:rhino:1.7.7.2 - https://developer.mozilla.org/en/Rhino)
Public Domain:
- * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.39.1 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client)
- * jersey-core-common (org.glassfish.jersey.core:jersey-common:2.39.1 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common)
- * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.39.1 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2)
+ * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client)
+ * jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common)
+ * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2)
+ * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart)
* HdrHistogram (org.hdrhistogram:HdrHistogram:2.1.12 - http://hdrhistogram.github.io/HdrHistogram/)
* JSON in Java (org.json:json:20231013 - https://github.com/douglascrockford/JSON-java)
* LatencyUtils (org.latencyutils:LatencyUtils:2.0.3 - http://latencyutils.github.io/LatencyUtils/)
@@ -667,10 +707,12 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines
W3C license:
- * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.39.1 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client)
- * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.39.1 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2)
+ * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client)
+ * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2)
+ * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart)
jQuery license:
- * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.39.1 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client)
- * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.39.1 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2)
+ * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client)
+ * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2)
+ * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart)
diff --git a/docker-compose-cli.yml b/docker-compose-cli.yml
index d2769added8d..8858c009193b 100644
--- a/docker-compose-cli.yml
+++ b/docker-compose-cli.yml
@@ -7,7 +7,7 @@ networks:
services:
dspace-cli:
# UMD Customization
- image: "docker.lib.umd.edu/drum-cli:${DSPACE_VER:-7_x}"
+ image: "docker.lib.umd.edu/drum-cli:${DSPACE_VER:-latest}"
# End UMD Customization
container_name: dspace-cli
build:
diff --git a/docker-compose.yml b/docker-compose.yml
index a22fb9ab5e9d..8032cfc80846 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -51,7 +51,7 @@ services:
JPDA_OPTS: "-agentlib:jdwp=transport=dt_socket,address=0.0.0.0:8000,server=y,suspend=n"
# End UMD Customization
# UMD Customization
- image: "docker.lib.umd.edu/drum:${DSPACE_VER:-7_x-dev}"
+ image: "docker.lib.umd.edu/drum:${DSPACE_VER:-8_x-dev}"
# End UMD Customization
build:
context: .
@@ -65,10 +65,8 @@ services:
ports:
- published: 8080
target: 8080
- # UMD Customization
- published: 8000
target: 8000
- # End UMD Customization
stdin_open: true
tty: true
volumes:
@@ -80,31 +78,31 @@ services:
# Ensure that the database is ready BEFORE starting tomcat
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
# 2. Then, run database migration to init database tables
- # 3. Finally, start Tomcat
+ # 3. Finally, start DSpace
entrypoint:
- /bin/bash
- '-c'
- # UMD Customization
- |
while (! /dev/null 2>&1; do sleep 1; done;
- /dspace/bin/dspace database migrate ignored
- catalina.sh jpda run
- # End UMD Customization
+ /dspace/bin/dspace database migrate
+ java -jar /dspace/webapps/server-boot.jar --dspace.dir=/dspace
# DSpace PostgreSQL database container
dspacedb:
container_name: dspacedb
- # UMD Customization
# Uses a custom Postgres image with pgcrypto installed
- image: docker.lib.umd.edu/dspace-postgres:latest
+ # UMD Customization
+ image: docker.lib.umd.edu/dspace-postgres:${DSPACE_VER:-latest}
+ # End UMD Customization
build:
# Must build out of subdirectory to have access to install script for pgcrypto
context: ./dspace/src/main/docker/dspace-postgres-pgcrypto/
environment:
PGDATA: /pgdata
+ # UMD Customization
POSTGRES_DB: drum
POSTGRES_USER: drum
POSTGRES_PASSWORD: drum
- # End UMD Customization
+ # End UMD Customization
networks:
dspacenet:
ports:
@@ -113,14 +111,15 @@ services:
stdin_open: true
tty: true
volumes:
+ # Keep Postgres data directory between reboots
+ - pgdata:/pgdata
# UMD Customization
- - pgdata:/pgdatasql
- ./postgres-init:/docker-entrypoint-initdb.d
# End UMD Customization
# DSpace Solr container
dspacesolr:
container_name: dspacesolr
- image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-7_x}"
+ image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}"
build:
context: ./dspace/src/main/docker/dspace-solr/
# Provide path to Solr configs necessary to build Docker image
@@ -156,6 +155,10 @@ services:
cp -r /opt/solr/server/solr/configsets/search/* search
precreate-core statistics /opt/solr/server/solr/configsets/statistics
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:
diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml
index ee9ad66e0754..9e8f7acc63c0 100644
--- a/dspace-api/pom.xml
+++ b/dspace-api/pom.xml
@@ -12,7 +12,7 @@
org.dspace
dspace-parent
- 7.6.2-drum-3-SNAPSHOT
+ 8.0-drum-0-SNAPSHOT
..
@@ -54,21 +54,21 @@
- org.hibernate
+ org.hibernate.orm
hibernate-jpamodelgen
${hibernate.version}
- javax.xml.bind
- jaxb-api
+ jakarta.xml.bind
+ jakarta.xml.bind-api
${jaxb-api.version}
- javax.annotation
- javax.annotation-api
- ${javax-annotation.version}
+ jakarta.annotation
+ jakarta.annotation-api
+ ${jakarta-annotation.version}
@@ -177,7 +177,7 @@
org.codehaus.mojo
jaxb2-maven-plugin
- 2.5.0
+ 3.1.0
workflow-curation
@@ -265,7 +265,7 @@
- ${agnostic.build.dir}/testing/dspace/
+ ${agnostic.build.dir}/testing/dspace
true
${agnostic.build.dir}/testing/dspace/solr/
@@ -324,7 +324,7 @@
- ${agnostic.build.dir}/testing/dspace/
+ ${agnostic.build.dir}/testing/dspace
true
${agnostic.build.dir}/testing/dspace/solr/
@@ -342,18 +342,15 @@
log4j-api
- org.hibernate
+ org.hibernate.orm
hibernate-core
-
-
-
- org.javassist
- javassist
-
-
- org.hibernate
+ org.hibernate.orm
+ hibernate-jpamodelgen
+
+
+ org.hibernate.orm
hibernate-jcache
@@ -375,23 +372,18 @@
- javax.cache
- cache-api
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
- org.hibernate
- hibernate-jpamodelgen
+ javax.cache
+ cache-api
org.hibernate.validator
hibernate-validator-cdi
${hibernate-validator.version}
-
- org.hibernate.javax.persistence
- hibernate-jpa-2.1-api
- 1.0.2.Final
-
org.springframework
@@ -402,30 +394,26 @@
net.handle
handle
+
net.cnri
cnri-servlet-container
+ runtime
-
-
- org.ow2.asm
- asm-commons
-
-
+
- org.bouncycastle
- bcpkix-jdk15on
-
-
- org.bouncycastle
- bcprov-jdk15on
+ org.mortbay.jasper
+ apache-jsp
-
+
+
org.eclipse.jetty
jetty-server
+ runtime
org.dspace
@@ -435,12 +423,6 @@
org.apache.jena
apache-jena-libs
pom
-
-
- log4j
- log4j
-
-
commons-cli
@@ -458,10 +440,6 @@
org.apache.commons
commons-dbcp2
-
- commons-fileupload
- commons-fileupload
-
commons-io
@@ -480,17 +458,26 @@
commons-validator
- com.sun.mail
- javax.mail
+ jakarta.mail
+ jakarta.mail-api
+ provided
+
+
+ org.eclipse.angus
+ jakarta.mail
- javax.servlet
- javax.servlet-api
+ jakarta.servlet
+ jakarta.servlet-api
provided
- javax.annotation
- javax.annotation-api
+ jakarta.annotation
+ jakarta.annotation-api
+
+
+ jakarta.el
+ jakarta.el-api
jaxen
@@ -586,6 +573,13 @@
solr-core
test
${solr.client.version}
+
+
+
+ org.antlr
+ antlr4-runtime
+
+
org.apache.lucene
@@ -665,7 +659,12 @@
org.flywaydb
flyway-core
- 8.5.13
+ ${flyway.version}
+
+
+ org.flywaydb
+ flyway-database-postgresql
+ ${flyway.version}
@@ -701,27 +700,22 @@
- joda-time
- joda-time
-
-
- javax.inject
- javax.inject
- 1
- jar
+ jakarta.inject
+ jakarta.inject-api
+ 2.0.1
- javax.xml.bind
- jaxb-api
+ jakarta.xml.bind
+ jakarta.xml.bind-api
org.glassfish.jaxb
jaxb-runtime
-
+
org.glassfish.jersey.core
jersey-client
@@ -742,31 +736,18 @@
1.12.261
+
+
- org.orcid
- orcid-model
- 3.0.2
+ org.dspace
+ orcid-model-jakarta
+ 3.3.0
-
- javax.validation
- validation-api
-
-
- com.fasterxml.jackson.jaxrs
- jackson-jaxrs-json-provider
-
-
- org.yaml
- snakeyaml
-
org.javassist
javassist
-
- io.swagger
- swagger-jersey-jaxrs
-
@@ -823,6 +804,12 @@
+
+
+ eu.openaire
+ broker-client
+ 1.1.2
+
org.mock-server
@@ -835,9 +822,23 @@
org.yaml
snakeyaml
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+
+
+
+ javax.xml.bind
+ jaxb-api
+
+
+
+ javax.servlet
+ javax.servlet-api
+
-
+
io.findify
s3mock_2.13
@@ -854,7 +855,6 @@
-
@@ -909,14 +909,9 @@
2.2.14
- jakarta.xml.bind
- jakarta.xml.bind-api
- 2.3.3
-
-
- javax.validation
- validation-api
- 2.0.1.Final
+ jakarta.validation
+ jakarta.validation-api
+ 3.0.2
io.swagger
diff --git a/dspace-api/src/main/java/org/dspace/access/status/AccessStatusServiceImpl.java b/dspace-api/src/main/java/org/dspace/access/status/AccessStatusServiceImpl.java
index e1f11285d840..01b370747932 100644
--- a/dspace-api/src/main/java/org/dspace/access/status/AccessStatusServiceImpl.java
+++ b/dspace-api/src/main/java/org/dspace/access/status/AccessStatusServiceImpl.java
@@ -8,6 +8,8 @@
package org.dspace.access.status;
import java.sql.SQLException;
+import java.time.LocalDate;
+import java.time.ZoneId;
import java.util.Date;
import org.dspace.access.status.service.AccessStatusService;
@@ -15,7 +17,6 @@
import org.dspace.core.Context;
import org.dspace.core.service.PluginService;
import org.dspace.services.ConfigurationService;
-import org.joda.time.LocalDate;
import org.springframework.beans.factory.annotation.Autowired;
/**
@@ -55,7 +56,10 @@ public void init() throws Exception {
int month = configurationService.getIntProperty("access.status.embargo.forever.month");
int day = configurationService.getIntProperty("access.status.embargo.forever.day");
- forever_date = new LocalDate(year, month, day).toDate();
+ forever_date = Date.from(LocalDate.of(year, month, day)
+ .atStartOfDay()
+ .atZone(ZoneId.systemDefault())
+ .toInstant());
}
}
diff --git a/dspace-api/src/main/java/org/dspace/administer/MetadataImporter.java b/dspace-api/src/main/java/org/dspace/administer/MetadataImporter.java
index 2677cb20501f..80bda610c7dd 100644
--- a/dspace-api/src/main/java/org/dspace/administer/MetadataImporter.java
+++ b/dspace-api/src/main/java/org/dspace/administer/MetadataImporter.java
@@ -21,6 +21,8 @@
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.MetadataField;
import org.dspace.content.MetadataSchema;
@@ -30,8 +32,6 @@
import org.dspace.content.service.MetadataFieldService;
import org.dspace.content.service.MetadataSchemaService;
import org.dspace.core.Context;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
@@ -40,9 +40,9 @@
/**
* @author Richard Jones
*
- * This class takes an xml document as passed in the arguments and
+ * This class takes an XML document as passed in the arguments and
* uses it to create metadata elements in the Metadata Registry if
- * they do not already exist
+ * they do not already exist.
*
* The format of the XML file is as follows:
*
@@ -69,7 +69,7 @@ public class MetadataImporter {
/**
* logging category
*/
- private static final Logger log = LoggerFactory.getLogger(MetadataImporter.class);
+ private static final Logger log = LogManager.getLogger();
/**
* Default constructor
@@ -89,6 +89,7 @@ private MetadataImporter() { }
* @throws SAXException if parser error
* @throws NonUniqueMetadataException if duplicate metadata
* @throws RegistryImportException if import fails
+ * @throws XPathExpressionException passed through
**/
public static void main(String[] args)
throws ParseException, SQLException, IOException, TransformerException,
@@ -125,6 +126,7 @@ public static void main(String[] args)
* @throws SAXException if parser error
* @throws NonUniqueMetadataException if duplicate metadata
* @throws RegistryImportException if import fails
+ * @throws XPathExpressionException passed through
*/
public static void loadRegistry(String file, boolean forceUpdate)
throws SQLException, IOException, TransformerException, ParserConfigurationException, AuthorizeException,
@@ -203,7 +205,7 @@ private static void loadSchema(Context context, Node node, boolean updateExistin
if (s == null) {
// Schema does not exist - create
- log.info("Registering Schema " + name + " (" + namespace + ")");
+ log.info("Registering Schema {}({})", name, namespace);
metadataSchemaService.create(context, name, namespace);
} else {
// Schema exists - if it's the same namespace, allow the type imports to continue
@@ -215,7 +217,7 @@ private static void loadSchema(Context context, Node node, boolean updateExistin
// It's a different namespace - have we been told to update?
if (updateExisting) {
// Update the existing schema namespace and continue to type import
- log.info("Updating Schema " + name + ": New namespace " + namespace);
+ log.info("Updating Schema {}: New namespace {}", name, namespace);
s.setNamespace(namespace);
metadataSchemaService.update(context, s);
} else {
@@ -274,7 +276,7 @@ private static void loadType(Context context, Node node)
if (qualifier == null) {
fieldName = schema + "." + element;
}
- log.info("Registering metadata field " + fieldName);
+ log.info("Registering metadata field {}", fieldName);
MetadataField field = metadataFieldService.create(context, schemaObj, element, qualifier, scopeNote);
metadataFieldService.update(context, field);
}
diff --git a/dspace-api/src/main/java/org/dspace/alerts/SystemWideAlert.java b/dspace-api/src/main/java/org/dspace/alerts/SystemWideAlert.java
index f56cbdcce9e9..432c633ea591 100644
--- a/dspace-api/src/main/java/org/dspace/alerts/SystemWideAlert.java
+++ b/dspace-api/src/main/java/org/dspace/alerts/SystemWideAlert.java
@@ -8,17 +8,17 @@
package org.dspace.alerts;
import java.util.Date;
-import javax.persistence.Cacheable;
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-import javax.persistence.SequenceGenerator;
-import javax.persistence.Table;
-import javax.persistence.Temporal;
-import javax.persistence.TemporalType;
+import jakarta.persistence.Cacheable;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.SequenceGenerator;
+import jakarta.persistence.Table;
+import jakarta.persistence.Temporal;
+import jakarta.persistence.TemporalType;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.dspace.core.ReloadableEntity;
diff --git a/dspace-api/src/main/java/org/dspace/alerts/dao/impl/SystemWideAlertDAOImpl.java b/dspace-api/src/main/java/org/dspace/alerts/dao/impl/SystemWideAlertDAOImpl.java
index 13a0e0af236a..79dc1bcf27a3 100644
--- a/dspace-api/src/main/java/org/dspace/alerts/dao/impl/SystemWideAlertDAOImpl.java
+++ b/dspace-api/src/main/java/org/dspace/alerts/dao/impl/SystemWideAlertDAOImpl.java
@@ -9,10 +9,10 @@
import java.sql.SQLException;
import java.util.List;
-import javax.persistence.criteria.CriteriaBuilder;
-import javax.persistence.criteria.CriteriaQuery;
-import javax.persistence.criteria.Root;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Root;
import org.dspace.alerts.SystemWideAlert;
import org.dspace.alerts.SystemWideAlert_;
import org.dspace.alerts.dao.SystemWideAlertDAO;
diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java
index ad46cb95c353..e8cf42b47c1b 100644
--- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java
+++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java
@@ -20,8 +20,8 @@
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
-import javax.annotation.Nullable;
+import jakarta.annotation.Nullable;
import org.apache.commons.cli.ParseException;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
diff --git a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java
index 7c80e1ea7dc6..9eaabc20e862 100644
--- a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java
+++ b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java
@@ -31,8 +31,8 @@
import java.util.UUID;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
-import javax.mail.MessagingException;
+import jakarta.mail.MessagingException;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.dspace.app.itemexport.service.ItemExportService;
diff --git a/dspace-api/src/main/java/org/dspace/app/itemexport/service/ItemExportService.java b/dspace-api/src/main/java/org/dspace/app/itemexport/service/ItemExportService.java
index 6ec1027709bb..cc53d952e79b 100644
--- a/dspace-api/src/main/java/org/dspace/app/itemexport/service/ItemExportService.java
+++ b/dspace-api/src/main/java/org/dspace/app/itemexport/service/ItemExportService.java
@@ -11,8 +11,8 @@
import java.util.Date;
import java.util.Iterator;
import java.util.List;
-import javax.mail.MessagingException;
+import jakarta.mail.MessagingException;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.core.Context;
diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java
index 1e219ee6314c..087a33026151 100644
--- a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java
+++ b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java
@@ -46,7 +46,6 @@
import java.util.UUID;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
-import javax.mail.MessagingException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
@@ -56,6 +55,7 @@
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
+import jakarta.mail.MessagingException;
import org.apache.commons.collections4.ComparatorUtils;
import org.apache.commons.io.FileDeleteStrategy;
import org.apache.commons.io.FileUtils;
diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/service/ItemImportService.java b/dspace-api/src/main/java/org/dspace/app/itemimport/service/ItemImportService.java
index e99ece31b9bb..90cb6f9b803a 100644
--- a/dspace-api/src/main/java/org/dspace/app/itemimport/service/ItemImportService.java
+++ b/dspace-api/src/main/java/org/dspace/app/itemimport/service/ItemImportService.java
@@ -10,8 +10,8 @@
import java.io.File;
import java.io.IOException;
import java.util.List;
-import javax.mail.MessagingException;
+import jakarta.mail.MessagingException;
import org.dspace.app.itemimport.BatchUpload;
import org.dspace.content.Collection;
import org.dspace.core.Context;
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/ItemFilter.java b/dspace-api/src/main/java/org/dspace/app/ldn/ItemFilter.java
new file mode 100644
index 000000000000..44dd5389d3de
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/ItemFilter.java
@@ -0,0 +1,30 @@
+/**
+ * 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;
+
+/**
+ * model class for the item filters configured into item-filters.xml
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+public class ItemFilter {
+
+ private String id;
+
+ public ItemFilter(String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageConsumer.java b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageConsumer.java
new file mode 100644
index 000000000000..210aaa6c9c97
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageConsumer.java
@@ -0,0 +1,243 @@
+/**
+ * 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.lang.String.format;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.UUID;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.dspace.app.ldn.factory.NotifyServiceFactory;
+import org.dspace.app.ldn.model.Notification;
+import org.dspace.app.ldn.service.LDNMessageService;
+import org.dspace.app.ldn.service.NotifyPatternToTriggerService;
+import org.dspace.app.ldn.service.NotifyServiceInboundPatternService;
+import org.dspace.content.Bitstream;
+import org.dspace.content.BitstreamFormat;
+import org.dspace.content.Bundle;
+import org.dspace.content.Item;
+import org.dspace.content.MetadataValue;
+import org.dspace.content.factory.ContentServiceFactory;
+import org.dspace.content.logic.LogicalStatement;
+import org.dspace.content.service.BitstreamService;
+import org.dspace.content.service.ItemService;
+import org.dspace.core.Constants;
+import org.dspace.core.Context;
+import org.dspace.core.I18nUtil;
+import org.dspace.core.LDN;
+import org.dspace.event.Consumer;
+import org.dspace.event.Event;
+import org.dspace.services.ConfigurationService;
+import org.dspace.services.factory.DSpaceServicesFactory;
+import org.dspace.utils.DSpace;
+import org.dspace.web.ContextUtil;
+
+/**
+ * class for creating a new LDN Messages of installed item
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+public class LDNMessageConsumer implements Consumer {
+
+ private NotifyPatternToTriggerService notifyPatternToTriggerService;
+ private NotifyServiceInboundPatternService inboundPatternService;
+ private LDNMessageService ldnMessageService;
+ private ConfigurationService configurationService;
+ private ItemService itemService;
+ private BitstreamService bitstreamService;
+
+ @Override
+ public void initialize() throws Exception {
+ notifyPatternToTriggerService = NotifyServiceFactory.getInstance().getNotifyPatternToTriggerService();
+ ldnMessageService = NotifyServiceFactory.getInstance().getLDNMessageService();
+ configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
+ itemService = ContentServiceFactory.getInstance().getItemService();
+ bitstreamService = ContentServiceFactory.getInstance().getBitstreamService();
+ inboundPatternService = NotifyServiceFactory.getInstance().getNotifyServiceInboundPatternService();
+ }
+
+ @Override
+ public void consume(Context context, Event event) throws Exception {
+
+ if (event.getSubjectType() != Constants.ITEM ||
+ event.getEventType() != Event.INSTALL) {
+ return;
+ }
+
+ Item item = (Item) event.getSubject(context);
+ createManualLDNMessages(context, item);
+ createAutomaticLDNMessages(context, item);
+ }
+
+ private void createManualLDNMessages(Context context, Item item) throws SQLException, JsonProcessingException {
+ List patternsToTrigger =
+ notifyPatternToTriggerService.findByItem(context, item);
+
+ for (NotifyPatternToTrigger patternToTrigger : patternsToTrigger) {
+ createLDNMessage(context,patternToTrigger.getItem(),
+ patternToTrigger.getNotifyService(), patternToTrigger.getPattern());
+ }
+ }
+
+ private void createAutomaticLDNMessages(Context context, Item item) throws SQLException, JsonProcessingException {
+
+ List inboundPatterns = inboundPatternService.findAutomaticPatterns(context);
+
+ for (NotifyServiceInboundPattern inboundPattern : inboundPatterns) {
+ if (StringUtils.isEmpty(inboundPattern.getConstraint()) ||
+ evaluateFilter(context, item, inboundPattern.getConstraint())) {
+ createLDNMessage(context, item, inboundPattern.getNotifyService(), inboundPattern.getPattern());
+ }
+ }
+ }
+
+ private boolean evaluateFilter(Context context, Item item, String constraint) {
+ LogicalStatement filter =
+ new DSpace().getServiceManager().getServiceByName(constraint, LogicalStatement.class);
+
+ return filter != null && filter.getResult(context, item);
+ }
+
+ private void createLDNMessage(Context context, Item item, NotifyServiceEntity service, String pattern)
+ throws SQLException, JsonMappingException, JsonProcessingException {
+
+ LDN ldn = getLDNMessage(pattern);
+ LDNMessageEntity ldnMessage =
+ ldnMessageService.create(context, format("urn:uuid:%s", UUID.randomUUID()));
+
+ ldnMessage.setObject(item);
+ ldnMessage.setTarget(service);
+ ldnMessage.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_QUEUED);
+ ldnMessage.setQueueTimeout(new Date());
+
+ appendGeneratedMessage(ldn, ldnMessage, pattern);
+
+ ObjectMapper mapper = new ObjectMapper();
+ Notification notification = mapper.readValue(ldnMessage.getMessage(), Notification.class);
+ ldnMessage.setType(StringUtils.joinWith(",", notification.getType()));
+
+ ArrayList notificationTypeArrayList = new ArrayList(notification.getType());
+ // sorting the list
+ Collections.sort(notificationTypeArrayList);
+ ldnMessage.setActivityStreamType(notificationTypeArrayList.get(0));
+ ldnMessage.setCoarNotifyType(notificationTypeArrayList.get(1));
+
+ ldnMessageService.update(context, ldnMessage);
+ }
+
+ private LDN getLDNMessage(String pattern) {
+ try {
+ return LDN.getLDNMessage(I18nUtil.getLDNFilename(Locale.getDefault(), pattern));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void appendGeneratedMessage(LDN ldn, LDNMessageEntity ldnMessage, String pattern) {
+ Item item = (Item) ldnMessage.getObject();
+ ldn.addArgument(getUiUrl());
+ ldn.addArgument(configurationService.getProperty("ldn.notify.inbox"));
+ ldn.addArgument(configurationService.getProperty("dspace.name"));
+ ldn.addArgument(Objects.requireNonNullElse(ldnMessage.getTarget().getUrl(), ""));
+ ldn.addArgument(Objects.requireNonNullElse(ldnMessage.getTarget().getLdnUrl(), ""));
+ ldn.addArgument(getUiUrl() + "/handle/" + ldnMessage.getObject().getHandle());
+ ldn.addArgument(getIdentifierUri(item));
+ ldn.addArgument(generateBitstreamDownloadUrl(item));
+ ldn.addArgument(getBitstreamMimeType(findPrimaryBitstream(item)));
+ ldn.addArgument(ldnMessage.getID());
+ ldn.addArgument(getRelationUri(item));
+ ldn.addArgument("http://purl.org/vocab/frbr/core#supplement");
+ ldn.addArgument(format("urn:uuid:%s", UUID.randomUUID()));
+
+ ldnMessage.setMessage(ldn.generateLDNMessage());
+ }
+
+ private String getUiUrl() {
+ return configurationService.getProperty("dspace.ui.url");
+ }
+
+ private String getIdentifierUri(Item item) {
+ return itemService.getMetadataByMetadataString(item, "dc.identifier.uri")
+ .stream()
+ .findFirst()
+ .map(MetadataValue::getValue)
+ .orElse("");
+ }
+
+ private String getRelationUri(Item item) {
+ String relationMetadata = configurationService.getProperty("ldn.notify.relation.metadata", "dc.relation");
+ return itemService.getMetadataByMetadataString(item, relationMetadata)
+ .stream()
+ .findFirst()
+ .map(MetadataValue::getValue)
+ .orElse("");
+ }
+
+ private String generateBitstreamDownloadUrl(Item item) {
+ String uiUrl = getUiUrl();
+ return findPrimaryBitstream(item)
+ .map(bs -> uiUrl + "/bitstreams/" + bs.getID() + "/download")
+ .orElse("");
+ }
+
+ private Optional findPrimaryBitstream(Item item) {
+ List bundles = item.getBundles(Constants.CONTENT_BUNDLE_NAME);
+ return bundles.stream()
+ .findFirst()
+ .map(Bundle::getPrimaryBitstream)
+ .or(() -> bundles.stream()
+ .findFirst()
+ .flatMap(bundle -> CollectionUtils.isNotEmpty(bundle.getBitstreams())
+ ? Optional.of(bundle.getBitstreams().get(0))
+ : Optional.empty()));
+ }
+
+ private String getBitstreamMimeType(Optional bitstream) {
+ return bitstream.map(bs -> {
+ try {
+ Context context = ContextUtil.obtainCurrentRequestContext();
+ BitstreamFormat bitstreamFormat = bs.getFormat(context);
+ if (bitstreamFormat.getShortDescription().equals("Unknown")) {
+ return getUserFormatMimeType(bs);
+ }
+ return bitstreamFormat.getMIMEType();
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }).orElse("");
+ }
+
+ private String getUserFormatMimeType(Bitstream bitstream) {
+ return bitstreamService.getMetadataFirstValue(bitstream,
+ "dc", "format", "mimetype", Item.ANY);
+ }
+
+ @Override
+ public void end(Context ctx) throws Exception {
+
+ }
+
+ @Override
+ public void finish(Context ctx) throws Exception {
+
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageEntity.java b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageEntity.java
new file mode 100644
index 000000000000..27257455e0ce
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageEntity.java
@@ -0,0 +1,319 @@
+/**
+ * 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 java.lang.reflect.Field;
+import java.util.Date;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import jakarta.persistence.Temporal;
+import jakarta.persistence.TemporalType;
+import org.dspace.content.DSpaceObject;
+import org.dspace.core.ReloadableEntity;
+
+/**
+ * Class representing ldnMessages stored in the DSpace system and, when locally resolvable,
+ * some information are stored as dedicated attributes.
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+@Entity
+@Table(name = "ldn_message")
+public class LDNMessageEntity implements ReloadableEntity {
+
+ /**
+ * LDN messages interact with a fictitious queue. Scheduled tasks manage the queue.
+ */
+
+ /*
+ * Notification Type constants
+ */
+ public static final String TYPE_INCOMING = "Incoming";
+ public static final String TYPE_OUTGOING = "Outgoing";
+
+ /**
+ * Message must not be processed.
+ */
+ public static final Integer QUEUE_STATUS_UNTRUSTED_IP = 0;
+
+ /**
+ * Message queued, it has to be elaborated.
+ */
+ public static final Integer QUEUE_STATUS_QUEUED = 1;
+
+ /**
+ * Message has been taken from the queue and it's elaboration is in progress.
+ */
+ public static final Integer QUEUE_STATUS_PROCESSING = 2;
+
+ /**
+ * Message has been correctly elaborated.
+ */
+ public static final Integer QUEUE_STATUS_PROCESSED = 3;
+
+ /**
+ * Message has not been correctly elaborated - despite more than "ldn.processor.max.attempts" retryies
+ */
+ public static final Integer QUEUE_STATUS_FAILED = 4;
+
+ /**
+ * Message must not be processed
+ */
+ public static final Integer QUEUE_STATUS_UNTRUSTED = 5;
+
+ /**
+ * Message is not processed since action is not mapped
+ */
+ public static final Integer QUEUE_STATUS_UNMAPPED_ACTION = 6;
+
+ /**
+ * Message queued for retry, it has to be elaborated.
+ */
+ public static final Integer QUEUE_STATUS_QUEUED_FOR_RETRY = 7;
+
+ @Id
+ private String id;
+
+ @ManyToOne
+ @JoinColumn(name = "object", referencedColumnName = "uuid")
+ private DSpaceObject object;
+
+ @Column(name = "message", columnDefinition = "text")
+ private String message;
+
+ @Column(name = "type")
+ private String type;
+
+ @Column(name = "queue_status")
+ private Integer queueStatus;
+
+ @Column(name = "queue_attempts")
+ private Integer queueAttempts = 0;
+
+ @Temporal(TemporalType.TIMESTAMP)
+ @Column(name = "queue_last_start_time")
+ private Date queueLastStartTime = null;
+
+ @Temporal(TemporalType.TIMESTAMP)
+ @Column(name = "queue_timeout")
+ private Date queueTimeout = null;
+
+ @ManyToOne
+ @JoinColumn(name = "origin", referencedColumnName = "id")
+ private NotifyServiceEntity origin;
+
+ @ManyToOne
+ @JoinColumn(name = "target", referencedColumnName = "id")
+ private NotifyServiceEntity target;
+
+ @ManyToOne
+ @JoinColumn(name = "inReplyTo", referencedColumnName = "id")
+ private LDNMessageEntity inReplyTo;
+
+ @ManyToOne
+ @JoinColumn(name = "context", referencedColumnName = "uuid")
+ private DSpaceObject context;
+
+ @Column(name = "activity_stream_type")
+ private String activityStreamType;
+
+ @Column(name = "coar_notify_type")
+ private String coarNotifyType;
+
+ @Column(name = "source_ip")
+ private String sourceIp;
+
+ protected LDNMessageEntity() {
+
+ }
+
+ public LDNMessageEntity(String id) {
+ this.id = id;
+ }
+
+ @Override
+ public String getID() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ /**
+ *
+ * @return the DSpace item related to this message
+ */
+ public DSpaceObject getObject() {
+ return object;
+ }
+
+ public void setObject(DSpaceObject object) {
+ this.object = object;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public String getActivityStreamType() {
+ return activityStreamType;
+ }
+
+ public void setActivityStreamType(String activityStreamType) {
+ this.activityStreamType = activityStreamType;
+ }
+
+ public String getCoarNotifyType() {
+ return coarNotifyType;
+ }
+
+ public void setCoarNotifyType(String coarNotifyType) {
+ this.coarNotifyType = coarNotifyType;
+ }
+
+ /**
+ *
+ * @return The originator of the activity, typically the service responsible for sending the notification
+ */
+ public NotifyServiceEntity getOrigin() {
+ return origin;
+ }
+
+ public void setOrigin(NotifyServiceEntity origin) {
+ this.origin = origin;
+ }
+
+ /**
+ *
+ * @return The intended destination of the activity, typically the service which consumes the notification
+ */
+ public NotifyServiceEntity getTarget() {
+ return target;
+ }
+
+ public void setTarget(NotifyServiceEntity target) {
+ this.target = target;
+ }
+
+ /**
+ *
+ * @return This property is used when the notification is a direct response to a previous notification;
+ * contains an {@link org.dspace.app.ldn.LDNMessageEntity#inReplyTo id}
+ */
+ public LDNMessageEntity getInReplyTo() {
+ return inReplyTo;
+ }
+
+ public void setInReplyTo(LDNMessageEntity inReplyTo) {
+ this.inReplyTo = inReplyTo;
+ }
+
+ /**
+ *
+ * @return This identifies another resource which is relevant to understanding the notification
+ */
+ public DSpaceObject getContext() {
+ return context;
+ }
+
+ public void setContext(DSpaceObject context) {
+ this.context = context;
+ }
+
+ public Integer getQueueStatus() {
+ return queueStatus;
+ }
+
+ public void setQueueStatus(Integer queueStatus) {
+ this.queueStatus = queueStatus;
+ }
+
+ public Integer getQueueAttempts() {
+ return queueAttempts;
+ }
+
+ public void setQueueAttempts(Integer queueAttempts) {
+ this.queueAttempts = queueAttempts;
+ }
+
+ public Date getQueueLastStartTime() {
+ return queueLastStartTime;
+ }
+
+ public void setQueueLastStartTime(Date queueLastStartTime) {
+ this.queueLastStartTime = queueLastStartTime;
+ }
+
+ public Date getQueueTimeout() {
+ return queueTimeout;
+ }
+
+ public void setQueueTimeout(Date queueTimeout) {
+ this.queueTimeout = queueTimeout;
+ }
+
+ public String getSourceIp() {
+ return sourceIp;
+ }
+
+ public void setSourceIp(String sourceIp) {
+ this.sourceIp = sourceIp;
+ }
+
+ @Override
+ public String toString() {
+ return "LDNMessage id:" + this.getID() + " typed:" + this.getType();
+ }
+
+ public static String getNotificationType(LDNMessageEntity ldnMessage) {
+ if (ldnMessage.getInReplyTo() != null || ldnMessage.getOrigin() != null) {
+ return TYPE_INCOMING;
+ }
+ return TYPE_OUTGOING;
+ }
+
+ public static String getServiceNameForNotifyServ(NotifyServiceEntity serviceEntity) {
+ if (serviceEntity != null) {
+ return serviceEntity.getName();
+ }
+ return "self";
+ }
+
+ public static String getQueueStatus(LDNMessageEntity ldnMessage) {
+ Class cl = LDNMessageEntity.class;
+ try {
+ for (Field f : cl.getDeclaredFields()) {
+ String fieldName = f.getName();
+ if (fieldName.startsWith("QUEUE_") && (f.get(null) == ldnMessage.getQueueStatus())) {
+ return fieldName;
+ }
+ }
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ return null;
+ }
+}
diff --git a/dspace-services/src/main/java/org/dspace/servicemanager/servlet/package-info.java b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageQueueStatus.java
similarity index 52%
rename from dspace-services/src/main/java/org/dspace/servicemanager/servlet/package-info.java
rename to dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageQueueStatus.java
index 652c887e0429..ad3dd36e69c5 100644
--- a/dspace-services/src/main/java/org/dspace/servicemanager/servlet/package-info.java
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageQueueStatus.java
@@ -5,10 +5,12 @@
*
* http://www.dspace.org/license/
*/
+package org.dspace.app.ldn;
-/**
- * Support for using DSpace Services in a servlet context. This is how the
- * kernel and services get started by the servlet container.
- */
+public enum LDNMessageQueueStatus {
-package org.dspace.servicemanager.servlet;
+ /**
+ * Resulting processing status of an LDN Message (aka queue management)
+ */
+ QUEUED, PROCESSING, PROCESSED, FAILED;
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/LDNMetadataFields.java b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMetadataFields.java
new file mode 100644
index 000000000000..67a87c144c83
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMetadataFields.java
@@ -0,0 +1,38 @@
+/**
+ * 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;
+
+/**
+ * Constants for LDN metadata fields
+ *
+ * @author Francesco Bacchelli (francesco.bacchelli at 4science.com)
+ */
+public final class LDNMetadataFields {
+
+ // schema and element are the same for each metadata of LDN coar-notify
+ public static final String SCHEMA = "coar";
+ public static final String ELEMENT = "notify";
+
+ // qualifiers
+ public static final String INITIALIZE = "initialize";
+ public static final String REQUEST_REVIEW = "requestreview";
+ public static final String REQUEST_ENDORSEMENT = "requestendorsement";
+ public static final String EXAMINATION = "examination";
+ public static final String REFUSED = "refused";
+ public static final String REVIEW = "review";
+ public static final String ENDORSMENT = "endorsement";
+ public static final String RELEASE = "release";
+
+ /**
+ *
+ */
+ private LDNMetadataFields() {
+
+ }
+
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/LDNQueueExtractor.java b/dspace-api/src/main/java/org/dspace/app/ldn/LDNQueueExtractor.java
new file mode 100644
index 000000000000..57a7cdfb07bf
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/LDNQueueExtractor.java
@@ -0,0 +1,54 @@
+/**
+ * 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 java.sql.SQLException;
+
+import org.apache.logging.log4j.Logger;
+import org.dspace.app.ldn.factory.LDNMessageServiceFactory;
+import org.dspace.app.ldn.service.LDNMessageService;
+import org.dspace.core.Context;
+
+/**
+ * LDN Message manager: scheduled task invoking extractAndProcessMessageFromQueue() of {@link LDNMessageService}
+ *
+ * @author Francesco Bacchelli (francesco.bacchelli at 4science dot it)
+ */
+public class LDNQueueExtractor {
+
+ private static final LDNMessageService ldnMessageService = LDNMessageServiceFactory.getInstance()
+ .getLDNMessageService();
+ private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(LDNQueueExtractor.class);
+
+ /**
+ * Default constructor
+ */
+ private LDNQueueExtractor() {
+ }
+
+ /**
+ * 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) {
+ log.info("Processed Messages x" + processed_messages);
+ }
+ context.complete();
+ return processed_messages;
+ }
+
+};
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/LDNQueueTimeoutChecker.java b/dspace-api/src/main/java/org/dspace/app/ldn/LDNQueueTimeoutChecker.java
new file mode 100644
index 000000000000..36a927d672bc
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/LDNQueueTimeoutChecker.java
@@ -0,0 +1,53 @@
+/**
+ * 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 java.sql.SQLException;
+
+import org.apache.logging.log4j.Logger;
+import org.dspace.app.ldn.factory.LDNMessageServiceFactory;
+import org.dspace.app.ldn.service.LDNMessageService;
+import org.dspace.core.Context;
+
+/**
+ * LDN Message manager: scheduled task invoking checkQueueMessageTimeout() of {@link LDNMessageService}
+ *
+ * @author Francesco Bacchelli (francesco.bacchelli at 4science dot it)
+ */
+public class LDNQueueTimeoutChecker {
+
+ private static final LDNMessageService ldnMessageService = LDNMessageServiceFactory.getInstance()
+ .getLDNMessageService();
+ private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(LDNQueueTimeoutChecker.class);
+
+ /**
+ * Default constructor
+ */
+ private LDNQueueTimeoutChecker() {
+ }
+
+ /**
+ * 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) {
+ log.info("Managed Messages x" + fixed_messages);
+ }
+ context.complete();
+ return fixed_messages;
+ }
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/LDNRouter.java b/dspace-api/src/main/java/org/dspace/app/ldn/LDNRouter.java
new file mode 100644
index 000000000000..14957aa503a3
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/LDNRouter.java
@@ -0,0 +1,91 @@
+/**
+ * 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 java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.Logger;
+import org.dspace.app.ldn.processor.LDNProcessor;
+
+/**
+ * Linked Data Notification router.
+ */
+public class LDNRouter {
+
+ private Map, LDNProcessor> incomingProcessors = new HashMap<>();
+ private Map, LDNProcessor> outcomingProcessors = new HashMap<>();
+ private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(LDNRouter.class);
+
+ /**
+ * Route notification to processor
+ *
+ * @return LDNProcessor processor to process notification, can be null
+ */
+ public LDNProcessor route(LDNMessageEntity ldnMessage) {
+ if (ldnMessage == null) {
+ log.warn("A null LDNMessage was received and could not be routed.");
+ return null;
+ }
+ if (StringUtils.isEmpty(ldnMessage.getType())) {
+ log.warn("LDNMessage " + ldnMessage + " was received. It has no type, so it couldn't be routed.");
+ return null;
+ }
+ Set ldnMessageTypeSet = new HashSet();
+ ldnMessageTypeSet.add(ldnMessage.getActivityStreamType());
+ ldnMessageTypeSet.add(ldnMessage.getCoarNotifyType());
+
+ LDNProcessor processor = null;
+ if (ldnMessage.getTarget() == null) {
+ processor = incomingProcessors.get(ldnMessageTypeSet);
+ } else if (ldnMessage.getOrigin() == null) {
+ processor = outcomingProcessors.get(ldnMessageTypeSet);
+ }
+
+ return processor;
+ }
+
+ /**
+ * Get all incoming routes.
+ *
+ * @return Map, LDNProcessor>
+ */
+ public Map, LDNProcessor> getIncomingProcessors() {
+ return incomingProcessors;
+ }
+
+ /**
+ * Set all incoming routes.
+ *
+ * @param incomingProcessors
+ */
+ public void setIncomingProcessors(Map, LDNProcessor> incomingProcessors) {
+ this.incomingProcessors = incomingProcessors;
+ }
+
+ /**
+ * Get all outcoming routes.
+ *
+ * @return Map, LDNProcessor>
+ */
+ public Map, LDNProcessor> getOutcomingProcessors() {
+ return outcomingProcessors;
+ }
+
+ /**
+ * Set all outcoming routes.
+ *
+ * @param outcomingProcessors
+ */
+ public void setOutcomingProcessors(Map, LDNProcessor> outcomingProcessors) {
+ this.outcomingProcessors = outcomingProcessors;
+ }
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/NotifyPatternToTrigger.java b/dspace-api/src/main/java/org/dspace/app/ldn/NotifyPatternToTrigger.java
new file mode 100644
index 000000000000..da23471a1c66
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/NotifyPatternToTrigger.java
@@ -0,0 +1,82 @@
+/**
+ * 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 jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.SequenceGenerator;
+import jakarta.persistence.Table;
+import org.dspace.content.Item;
+import org.dspace.core.ReloadableEntity;
+
+/**
+ * Database object representing notify patterns to be triggered
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+@Entity
+@Table(name = "notifypatterns_to_trigger")
+public class NotifyPatternToTrigger implements ReloadableEntity {
+
+ @Id
+ @Column(name = "id")
+ @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "notifypatterns_to_trigger_id_seq")
+ @SequenceGenerator(name = "notifypatterns_to_trigger_id_seq",
+ sequenceName = "notifypatterns_to_trigger_id_seq",
+ allocationSize = 1)
+ private Integer id;
+
+ @ManyToOne
+ @JoinColumn(name = "item_id", referencedColumnName = "uuid")
+ private Item item;
+
+ @ManyToOne
+ @JoinColumn(name = "service_id", referencedColumnName = "id")
+ private NotifyServiceEntity notifyService;
+
+ @Column(name = "pattern")
+ private String pattern;
+
+ public void setId(Integer id) {
+ this.id = id;
+ }
+
+ public Item getItem() {
+ return item;
+ }
+
+ public void setItem(Item item) {
+ this.item = item;
+ }
+
+ public NotifyServiceEntity getNotifyService() {
+ return notifyService;
+ }
+
+ public void setNotifyService(NotifyServiceEntity notifyService) {
+ this.notifyService = notifyService;
+ }
+
+ public String getPattern() {
+ return pattern;
+ }
+
+ public void setPattern(String pattern) {
+ this.pattern = pattern;
+ }
+
+ @Override
+ public Integer getID() {
+ return id;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/NotifyServiceEntity.java b/dspace-api/src/main/java/org/dspace/app/ldn/NotifyServiceEntity.java
new file mode 100644
index 000000000000..c939256b52ba
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/NotifyServiceEntity.java
@@ -0,0 +1,156 @@
+/**
+ * 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 java.math.BigDecimal;
+import java.util.List;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.SequenceGenerator;
+import jakarta.persistence.Table;
+import org.dspace.core.ReloadableEntity;
+
+/**
+ * Database object representing notify services
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+@Entity
+@Table(name = "notifyservice")
+public class NotifyServiceEntity implements ReloadableEntity {
+
+ @Id
+ @Column(name = "id")
+ @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "notifyservice_id_seq")
+ @SequenceGenerator(name = "notifyservice_id_seq", sequenceName = "notifyservice_id_seq",
+ allocationSize = 1)
+ private Integer id;
+
+ @Column(name = "name", nullable = false)
+ private String name;
+
+ @Column(name = "description", columnDefinition = "text")
+ private String description;
+
+ @Column(name = "url")
+ private String url;
+
+ @Column(name = "ldn_url")
+ private String ldnUrl;
+
+ @OneToMany(mappedBy = "notifyService")
+ private List inboundPatterns;
+
+ @Column(name = "enabled")
+ private boolean enabled = false;
+
+ @Column(name = "score")
+ private BigDecimal score;
+
+ @Column(name = "lower_ip")
+ private String lowerIp;
+
+ @Column(name = "upper_ip")
+ private String upperIp;
+
+ public void setId(Integer id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ /**
+ * @return URL of an informative website
+ */
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ /**
+ * @return URL of the LDN InBox
+ */
+ public String getLdnUrl() {
+ return ldnUrl;
+ }
+
+ public void setLdnUrl(String ldnUrl) {
+ this.ldnUrl = ldnUrl;
+ }
+
+ /**
+ * @return The list of the inbound patterns configuration supported by the service
+ */
+ public List getInboundPatterns() {
+ return inboundPatterns;
+ }
+
+ public void setInboundPatterns(List inboundPatterns) {
+ this.inboundPatterns = inboundPatterns;
+ }
+
+ @Override
+ public Integer getID() {
+ return id;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public BigDecimal getScore() {
+ return score;
+ }
+
+ public void setScore(BigDecimal score) {
+ this.score = score;
+ }
+
+ public String getLowerIp() {
+ return lowerIp;
+ }
+
+ public void setLowerIp(String lowerIp) {
+ this.lowerIp = lowerIp;
+ }
+
+ public String getUpperIp() {
+ return upperIp;
+ }
+
+ public void setUpperIp(String upperIp) {
+ this.upperIp = upperIp;
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/NotifyServiceInboundPattern.java b/dspace-api/src/main/java/org/dspace/app/ldn/NotifyServiceInboundPattern.java
new file mode 100644
index 000000000000..329d6cb11cec
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/NotifyServiceInboundPattern.java
@@ -0,0 +1,103 @@
+/**
+ * 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 jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.SequenceGenerator;
+import jakarta.persistence.Table;
+import org.dspace.core.ReloadableEntity;
+
+/**
+ * Database object representing notify service inbound patterns. Every {@link org.dspace.app.ldn.NotifyServiceEntity}
+ * may have inbounds and outbounds. Inbounds are to be sent to the external service.
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+@Entity
+@Table(name = "notifyservice_inbound_pattern")
+public class NotifyServiceInboundPattern implements ReloadableEntity {
+
+ @Id
+ @Column(name = "id")
+ @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "notifyservice_inbound_pattern_id_seq")
+ @SequenceGenerator(name = "notifyservice_inbound_pattern_id_seq",
+ sequenceName = "notifyservice_inbound_pattern_id_seq",
+ allocationSize = 1)
+ private Integer id;
+
+ @ManyToOne
+ @JoinColumn(name = "service_id", referencedColumnName = "id")
+ private NotifyServiceEntity notifyService;
+
+ @Column(name = "pattern")
+ private String pattern;
+
+ @Column(name = "constraint_name")
+ private String constraint;
+
+ @Column(name = "automatic")
+ private boolean automatic;
+
+ @Override
+ public Integer getID() {
+ return id;
+ }
+
+ public void setId(Integer id) {
+ this.id = id;
+ }
+
+ public NotifyServiceEntity getNotifyService() {
+ return notifyService;
+ }
+
+ public void setNotifyService(NotifyServiceEntity notifyService) {
+ this.notifyService = notifyService;
+ }
+
+ /**
+ * @see coar documentation
+ * @return pattern of the inbound notification
+ */
+ public String getPattern() {
+ return pattern;
+ }
+
+ public void setPattern(String pattern) {
+ this.pattern = pattern;
+ }
+
+ /**
+ * @return the condition checked for automatic evaluation
+ */
+ public String getConstraint() {
+ return constraint;
+ }
+
+ public void setConstraint(String constraint) {
+ this.constraint = constraint;
+ }
+
+ /**
+ * when true - the notification is automatically when constraints are respected.
+ * @return the automatic flag
+ */
+ public boolean isAutomatic() {
+ return automatic;
+ }
+
+ public void setAutomatic(boolean automatic) {
+ this.automatic = automatic;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNAction.java b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNAction.java
new file mode 100644
index 000000000000..b0c895de9958
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNAction.java
@@ -0,0 +1,31 @@
+/**
+ * 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.action;
+
+import org.dspace.app.ldn.model.Notification;
+import org.dspace.content.Item;
+import org.dspace.core.Context;
+
+/**
+ * An action that is run after a notification has been processed.
+ */
+public interface LDNAction {
+
+ /**
+ * Execute action for provided notification and item corresponding to the
+ * notification context.
+ *
+ *@param context the context
+ * @param notification the processed notification to perform action against
+ * @param item the item corresponding to the notification context
+ * @return ActionStatus the resulting status of the action
+ * @throws Exception general exception that can be thrown while executing action
+ */
+ public LDNActionStatus execute(Context context, Notification notification, Item item) throws Exception;
+
+}
\ No newline at end of file
diff --git a/dspace-rest/src/main/java/org/dspace/rest/filter/ItemFilterList.java b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNActionStatus.java
similarity index 60%
rename from dspace-rest/src/main/java/org/dspace/rest/filter/ItemFilterList.java
rename to dspace-api/src/main/java/org/dspace/app/ldn/action/LDNActionStatus.java
index f6590e36f8e2..86f56ed9baab 100644
--- a/dspace-rest/src/main/java/org/dspace/rest/filter/ItemFilterList.java
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNActionStatus.java
@@ -5,8 +5,11 @@
*
* http://www.dspace.org/license/
*/
-package org.dspace.rest.filter;
+package org.dspace.app.ldn.action;
-public interface ItemFilterList {
- public ItemFilterTest[] getFilters();
-}
+/**
+ * Resulting status of an execution of an action.
+ */
+public enum LDNActionStatus {
+ CONTINUE, ABORT;
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNCorrectionAction.java b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNCorrectionAction.java
new file mode 100644
index 000000000000..5ce3804bcee8
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNCorrectionAction.java
@@ -0,0 +1,108 @@
+/**
+ * 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.action;
+
+import java.math.BigDecimal;
+import java.sql.SQLException;
+import java.util.Date;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.dspace.app.ldn.NotifyServiceEntity;
+import org.dspace.app.ldn.model.Notification;
+import org.dspace.app.ldn.service.LDNMessageService;
+import org.dspace.content.Item;
+import org.dspace.content.QAEvent;
+import org.dspace.content.service.ItemService;
+import org.dspace.core.Context;
+import org.dspace.handle.service.HandleService;
+import org.dspace.qaevent.service.QAEventService;
+import org.dspace.qaevent.service.dto.NotifyMessageDTO;
+import org.dspace.services.ConfigurationService;
+import org.springframework.beans.factory.annotation.Autowired;
+
+
+/**
+ * Implementation for LDN Correction Action. It creates a QA Event according to the LDN Message received *
+ * @author Francesco Bacchelli (francesco.bacchelli at 4science.it)
+ *
+ */
+public class LDNCorrectionAction implements LDNAction {
+
+ private static final Logger log = LogManager.getLogger(LDNEmailAction.class);
+
+ private String qaEventTopic;
+
+ @Autowired
+ private ConfigurationService configurationService;
+ @Autowired
+ protected ItemService itemService;
+ @Autowired
+ private QAEventService qaEventService;
+ @Autowired
+ private LDNMessageService ldnMessageService;
+ @Autowired
+ private HandleService handleService;
+
+ @Override
+ 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) {
+ String citeAs = notification.getObject().getIetfCiteAs();
+ if (citeAs == null || citeAs.isEmpty()) {
+ citeAs = notification.getObject().getId();
+ }
+ NotifyMessageDTO message = new NotifyMessageDTO();
+ message.setHref(citeAs);
+ message.setRelationship(notification.getObject().getAsRelationship());
+ if (notification.getOrigin() != null) {
+ message.setServiceId(notification.getOrigin().getId());
+ message.setServiceName(notification.getOrigin().getInbox());
+ }
+ 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,
+ mapper.writeValueAsString(message),
+ new Date());
+ qaEventService.store(context, qaEvent);
+ result = LDNActionStatus.CONTINUE;
+ }
+
+ return result;
+ }
+
+ private BigDecimal getScore(Context context, Notification notification) throws SQLException {
+
+ if (notification.getOrigin() == null) {
+ return BigDecimal.ZERO;
+ }
+
+ NotifyServiceEntity service = ldnMessageService.findNotifyService(context, notification.getOrigin());
+
+ if (service == null) {
+ return BigDecimal.ZERO;
+ }
+
+ return service.getScore();
+ }
+
+ public String getQaEventTopic() {
+ return qaEventTopic;
+ }
+
+ public void setQaEventTopic(String qaEventTopic) {
+ this.qaEventTopic = qaEventTopic;
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNEmailAction.java b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNEmailAction.java
new file mode 100644
index 000000000000..b87001f81500
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNEmailAction.java
@@ -0,0 +1,155 @@
+/**
+ * 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.action;
+
+import static java.lang.String.format;
+
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.dspace.app.ldn.model.Notification;
+import org.dspace.content.Item;
+import org.dspace.core.Context;
+import org.dspace.core.Email;
+import org.dspace.core.I18nUtil;
+import org.dspace.services.ConfigurationService;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Action to send email to receipients provided in actionSendFilter. The email
+ * body will be result of templating actionSendFilter.
+ */
+public class LDNEmailAction implements LDNAction {
+
+ private static final Logger log = LogManager.getLogger(LDNEmailAction.class);
+
+ private final static String DATE_PATTERN = "dd-MM-yyyy HH:mm:ss";
+
+ @Autowired
+ private ConfigurationService configurationService;
+
+ /*
+ * Supported for actionSendFilter are:
+ * -
+ * - GROUP:
+ * - SUBMITTER
+ */
+ private String actionSendFilter;
+
+ // The file name for the requested email
+ private String actionSendEmailTextFile;
+
+ /**
+ * Execute sending an email.
+ *
+ * Template context parameters:
+ *
+ * {0} Service Name
+ * {1} Item Name
+ * {2} Service URL
+ * {3} Item URL
+ * {4} Submitter's Name
+ * {5} Date of the received LDN notification
+ * {6} LDN notification
+ * {7} Item
+ *
+ * @param notification
+ * @param item
+ * @return ActionStatus
+ * @throws Exception
+ */
+ @Override
+ 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));
+
+ // Setting recipients email
+ for (String recipient : retrieveRecipientsEmail(item)) {
+ email.addRecipient(recipient);
+ }
+
+ String date = new SimpleDateFormat(DATE_PATTERN).format(Calendar.getInstance().getTime());
+
+ email.addArgument(notification.getActor().getName());
+ email.addArgument(item.getName());
+ email.addArgument(notification.getActor().getId());
+ email.addArgument(notification.getContext() != null ?
+ notification.getContext().getId() : notification.getObject().getId());
+ email.addArgument(item.getSubmitter().getFullName());
+ email.addArgument(date);
+ email.addArgument(notification);
+ email.addArgument(item);
+
+ email.send();
+ } catch (Exception e) {
+ log.error("An Error Occurred while sending a notification email", e);
+ }
+
+ return LDNActionStatus.CONTINUE;
+ }
+
+ /**
+ * @return String
+ */
+ public String getActionSendFilter() {
+ return actionSendFilter;
+ }
+
+ /**
+ * @param actionSendFilter
+ */
+ public void setActionSendFilter(String actionSendFilter) {
+ this.actionSendFilter = actionSendFilter;
+ }
+
+ /**
+ * @return String
+ */
+ public String getActionSendEmailTextFile() {
+ return actionSendEmailTextFile;
+ }
+
+ /**
+ * @param actionSendEmailTextFile
+ */
+ public void setActionSendEmailTextFile(String actionSendEmailTextFile) {
+ this.actionSendEmailTextFile = actionSendEmailTextFile;
+ }
+
+ /**
+ * Parses actionSendFilter for reserved tokens and returns list of email
+ * recipients.
+ *
+ * @param item the item which to get submitter email
+ * @return List list of email recipients
+ */
+ private List retrieveRecipientsEmail(Item item) {
+ List recipients = new LinkedList();
+
+ if (actionSendFilter.startsWith("SUBMITTER")) {
+ recipients.add(item.getSubmitter().getEmail());
+ } else if (actionSendFilter.startsWith("GROUP:")) {
+ String groupName = actionSendFilter.replace("GROUP:", "");
+ String property = format("email.%s.list", groupName);
+ String[] groupEmails = configurationService.getArrayProperty(property);
+ recipients = Arrays.asList(groupEmails);
+ } else {
+ recipients.add(actionSendFilter);
+ }
+
+ return recipients;
+ }
+
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNRelationCorrectionAction.java b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNRelationCorrectionAction.java
new file mode 100644
index 000000000000..f11a42ab2f90
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNRelationCorrectionAction.java
@@ -0,0 +1,110 @@
+/**
+ * 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.action;
+
+import java.math.BigDecimal;
+import java.sql.SQLException;
+import java.util.Date;
+import java.util.Set;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.dspace.app.ldn.NotifyServiceEntity;
+import org.dspace.app.ldn.model.Notification;
+import org.dspace.app.ldn.service.LDNMessageService;
+import org.dspace.content.Item;
+import org.dspace.content.QAEvent;
+import org.dspace.content.service.ItemService;
+import org.dspace.core.Context;
+import org.dspace.handle.service.HandleService;
+import org.dspace.qaevent.service.QAEventService;
+import org.dspace.qaevent.service.dto.NotifyMessageDTO;
+import org.dspace.services.ConfigurationService;
+import org.springframework.beans.factory.annotation.Autowired;
+
+
+/**
+ * Implementation for LDN Correction Action. It creates a QA Event according to the LDN Message received *
+ * @author Francesco Bacchelli (francesco.bacchelli at 4science.it)
+ *
+ */
+public class LDNRelationCorrectionAction implements LDNAction {
+
+ private static final Logger log = LogManager.getLogger(LDNEmailAction.class);
+
+ private String qaEventTopic;
+
+ @Autowired
+ private ConfigurationService configurationService;
+ @Autowired
+ protected ItemService itemService;
+ @Autowired
+ private QAEventService qaEventService;
+ @Autowired
+ private LDNMessageService ldnMessageService;
+ @Autowired
+ private HandleService handleService;
+
+ @Override
+ 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();
+ if (notification.getType().containsAll(Set.of("Announce",
+ "coar-notify:RelationshipAction"))) {
+ message.setHref(notification.getObject().getAsSubject());
+ } else {
+ message.setHref(notification.getObject().getAsObject());
+ }
+ message.setRelationship(notification.getObject().getAsRelationship());
+ if (notification.getOrigin() != null) {
+ message.setServiceId(notification.getOrigin().getId());
+ message.setServiceName(notification.getOrigin().getInbox());
+ }
+ 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,
+ mapper.writeValueAsString(message),
+ new Date());
+ qaEventService.store(context, qaEvent);
+ result = LDNActionStatus.CONTINUE;
+ }
+
+ return result;
+ }
+
+ private BigDecimal getScore(Context context, Notification notification) throws SQLException {
+
+ if (notification.getOrigin() == null) {
+ return BigDecimal.ZERO;
+ }
+
+ NotifyServiceEntity service = ldnMessageService.findNotifyService(context, notification.getOrigin());
+
+ if (service == null) {
+ return BigDecimal.ZERO;
+ }
+
+ return service.getScore();
+ }
+
+ public String getQaEventTopic() {
+ return qaEventTopic;
+ }
+
+ public void setQaEventTopic(String qaEventTopic) {
+ this.qaEventTopic = qaEventTopic;
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/action/SendLDNMessageAction.java b/dspace-api/src/main/java/org/dspace/app/ldn/action/SendLDNMessageAction.java
new file mode 100644
index 000000000000..c0ecf04304b8
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/action/SendLDNMessageAction.java
@@ -0,0 +1,118 @@
+/**
+ * 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.action;
+
+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.model.Notification;
+import org.dspace.content.Item;
+import org.dspace.core.Context;
+
+/**
+ * Action to send LDN Message
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+public class SendLDNMessageAction implements LDNAction {
+
+ private static final Logger log = LogManager.getLogger(SendLDNMessageAction.class);
+
+ private CloseableHttpClient client = null;
+
+ public SendLDNMessageAction() {
+ HttpClientBuilder builder = HttpClientBuilder.create();
+ client = builder
+ .disableAutomaticRetries()
+ .setMaxConnTotal(5)
+ .build();
+ }
+
+ public SendLDNMessageAction(CloseableHttpClient client) {
+ this();
+ if (client != null) {
+ this.client = client;
+ }
+ }
+
+ @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 isSuccessful(int statusCode) {
+ return statusCode == HttpStatus.SC_ACCEPTED ||
+ statusCode == HttpStatus.SC_CREATED;
+ }
+
+ 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 {
+ 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 LDNActionStatus.ABORT;
+ }
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/dao/LDNMessageDao.java b/dspace-api/src/main/java/org/dspace/app/ldn/dao/LDNMessageDao.java
new file mode 100644
index 000000000000..fcbb485acacc
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/dao/LDNMessageDao.java
@@ -0,0 +1,76 @@
+/**
+ * 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.dao;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import org.dspace.app.ldn.LDNMessageEntity;
+import org.dspace.content.Item;
+import org.dspace.core.Context;
+import org.dspace.core.GenericDAO;
+
+/**
+ * Database Access Object interface class for the LDNMessage object.
+ *
+ * The implementation of this class is responsible for all database calls for
+ * the LDNMessage object and is autowired by spring
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+public interface LDNMessageDao extends GenericDAO {
+
+ /**
+ * 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 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 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 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 findAllRelatedMessagesByItem(
+ Context context, LDNMessageEntity msg, Item item, String... relatedTypes) throws SQLException;
+
+ /**
+ *
+ * @param context
+ * @return the list of messages in need to be reprocessed - with queue_status as QUEUE_STATUS_QUEUED_FOR_RETRY
+ * @throws SQLException
+ */
+ public List findMessagesToBeReprocessed(Context context) throws SQLException;
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/dao/NotifyPatternToTriggerDao.java b/dspace-api/src/main/java/org/dspace/app/ldn/dao/NotifyPatternToTriggerDao.java
new file mode 100644
index 000000000000..9ecd1b728a4e
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/dao/NotifyPatternToTriggerDao.java
@@ -0,0 +1,49 @@
+/**
+ * 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.dao;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import org.dspace.app.ldn.NotifyPatternToTrigger;
+import org.dspace.content.Item;
+import org.dspace.core.Context;
+import org.dspace.core.GenericDAO;
+
+/**
+ * This is the Data Access Object for the {@link NotifyPatternToTrigger} object
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+public interface NotifyPatternToTriggerDao extends GenericDAO {
+
+ /**
+ * find the NotifyPatternToTrigger matched with the provided item
+ *
+ * @param context the context
+ * @param item the item
+ * @return the NotifyPatternToTrigger matched the provided item
+ * @throws SQLException if database error
+ */
+ public List findByItem(Context context, Item item) throws SQLException;
+
+ /**
+ * find the NotifyPatternToTrigger matched with the provided
+ * item and pattern
+ *
+ * @param context the context
+ * @param item the item
+ * @param pattern the pattern
+ * @return the NotifyPatternToTrigger matched the provided
+ * item and pattern
+ * @throws SQLException if database error
+ */
+ public List findByItemAndPattern(Context context, Item item, String pattern)
+ throws SQLException;
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/dao/NotifyServiceDao.java b/dspace-api/src/main/java/org/dspace/app/ldn/dao/NotifyServiceDao.java
new file mode 100644
index 000000000000..9751b3038290
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/dao/NotifyServiceDao.java
@@ -0,0 +1,45 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.app.ldn.dao;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import org.dspace.app.ldn.NotifyServiceEntity;
+import org.dspace.core.Context;
+import org.dspace.core.GenericDAO;
+
+/**
+ * This is the Data Access Object for the {@link NotifyServiceEntity} object
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+public interface NotifyServiceDao extends GenericDAO {
+ /**
+ * find the NotifyServiceEntity matched with the provided ldnUrl
+ *
+ * @param context the context
+ * @param ldnUrl the ldnUrl
+ * @return the NotifyServiceEntity matched the provided ldnUrl
+ * @throws SQLException if database error
+ */
+ public NotifyServiceEntity findByLdnUrl(Context context, String ldnUrl) throws SQLException;
+
+ /**
+ * find all NotifyServiceEntity matched the provided inbound pattern
+ * from the related notifyServiceInboundPatterns
+ * also with 'automatic' equals to false
+ *
+ * @param context the context
+ * @param pattern the ldnUrl
+ * @return all NotifyServiceEntity matched the provided pattern
+ * @throws SQLException if database error
+ */
+ public List findManualServicesByInboundPattern(Context context, String pattern)
+ throws SQLException;
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/dao/NotifyServiceInboundPatternDao.java b/dspace-api/src/main/java/org/dspace/app/ldn/dao/NotifyServiceInboundPatternDao.java
new file mode 100644
index 000000000000..194d30e79598
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/dao/NotifyServiceInboundPatternDao.java
@@ -0,0 +1,47 @@
+/**
+ * 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.dao;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import org.dspace.app.ldn.NotifyServiceEntity;
+import org.dspace.app.ldn.NotifyServiceInboundPattern;
+import org.dspace.core.Context;
+import org.dspace.core.GenericDAO;
+
+/**
+ * This is the Data Access Object for the {@link NotifyServiceInboundPattern} object
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+public interface NotifyServiceInboundPatternDao extends GenericDAO {
+
+ /**
+ * find all notifyServiceInboundPatterns matched with
+ * the provided notifyServiceEntity and pattern
+ *
+ * @param context the context
+ * @param notifyServiceEntity the notifyServiceEntity
+ * @param pattern the pattern
+ * @return all notifyServiceInboundPatterns matched with
+ * the provided notifyServiceEntity and pattern
+ * @throws SQLException if database error
+ */
+ public NotifyServiceInboundPattern findByServiceAndPattern(Context context,
+ NotifyServiceEntity notifyServiceEntity,
+ String pattern) throws SQLException;
+ /**
+ * find all automatic notifyServiceInboundPatterns
+ *
+ * @param context the context
+ * @return all automatic notifyServiceInboundPatterns
+ * @throws SQLException if database error
+ */
+ List findAutomaticPatterns(Context context) throws SQLException;
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/LDNMessageDaoImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/LDNMessageDaoImpl.java
new file mode 100644
index 000000000000..d811f6d39f34
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/LDNMessageDaoImpl.java
@@ -0,0 +1,169 @@
+/**
+ * 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.dao.impl;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Order;
+import jakarta.persistence.criteria.Predicate;
+import jakarta.persistence.criteria.Root;
+import org.apache.logging.log4j.Logger;
+import org.dspace.app.ldn.LDNMessageEntity;
+import org.dspace.app.ldn.LDNMessageEntity_;
+import org.dspace.app.ldn.dao.LDNMessageDao;
+import org.dspace.content.Item;
+import org.dspace.core.AbstractHibernateDAO;
+import org.dspace.core.Context;
+
+/**
+ * Hibernate implementation of the Database Access Object interface class for
+ * the LDNMessage object. This class is responsible for all database calls for
+ * the LDNMessage object and is autowired by spring
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+public class LDNMessageDaoImpl extends AbstractHibernateDAO implements LDNMessageDao {
+
+ private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(LDNMessageDaoImpl.class);
+
+ @Override
+ public List findOldestMessageToProcess(Context context, int max_attempts) throws SQLException {
+ CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
+ CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, LDNMessageEntity.class);
+ Root root = criteriaQuery.from(LDNMessageEntity.class);
+ criteriaQuery.select(root);
+ List andPredicates = new ArrayList<>(3);
+ andPredicates
+ .add(criteriaBuilder.equal(root.get(LDNMessageEntity_.queueStatus), LDNMessageEntity.QUEUE_STATUS_QUEUED));
+ andPredicates.add(criteriaBuilder.lessThan(root.get(LDNMessageEntity_.queueAttempts), max_attempts));
+ andPredicates.add(criteriaBuilder.lessThan(root.get(LDNMessageEntity_.queueTimeout), new Date()));
+ criteriaQuery.where(criteriaBuilder.and(andPredicates.toArray(new Predicate[] {})));
+ List orderList = new LinkedList<>();
+ orderList.add(criteriaBuilder.desc(root.get(LDNMessageEntity_.queueAttempts)));
+ orderList.add(criteriaBuilder.asc(root.get(LDNMessageEntity_.queueLastStartTime)));
+ criteriaQuery.orderBy(orderList);
+ List result = list(context, criteriaQuery, false, LDNMessageEntity.class, -1, -1);
+ if (result == null || result.isEmpty()) {
+ log.debug("No LDN messages found to be processed");
+ }
+ return result;
+ }
+
+ @Override
+ public List findMessagesToBeReprocessed(Context context) throws SQLException {
+ // looking for LDN Messages to be reprocessed message
+ CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
+ CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, LDNMessageEntity.class);
+ Root root = criteriaQuery.from(LDNMessageEntity.class);
+ criteriaQuery.select(root);
+ List andPredicates = new ArrayList<>(1);
+ andPredicates
+ .add(criteriaBuilder.equal(root.get(LDNMessageEntity_.queueStatus),
+ LDNMessageEntity.QUEUE_STATUS_QUEUED_FOR_RETRY));
+ criteriaQuery.where(criteriaBuilder.and(andPredicates.toArray(new Predicate[] {})));
+ List orderList = new LinkedList<>();
+ orderList.add(criteriaBuilder.desc(root.get(LDNMessageEntity_.queueAttempts)));
+ orderList.add(criteriaBuilder.asc(root.get(LDNMessageEntity_.queueLastStartTime)));
+ criteriaQuery.orderBy(orderList);
+ List result = list(context, criteriaQuery, false, LDNMessageEntity.class, -1, -1);
+ if (result == null || result.isEmpty()) {
+ log.debug("No LDN messages found to be processed");
+ }
+ return result;
+ }
+
+ @Override
+ public List findProcessingTimedoutMessages(Context context, int max_attempts)
+ throws SQLException {
+ CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
+ CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, LDNMessageEntity.class);
+ Root root = criteriaQuery.from(LDNMessageEntity.class);
+ criteriaQuery.select(root);
+ List andPredicates = new ArrayList<>(3);
+ andPredicates.add(
+ criteriaBuilder.equal(root.get(LDNMessageEntity_.queueStatus), LDNMessageEntity.QUEUE_STATUS_PROCESSING));
+ andPredicates.add(criteriaBuilder.lessThanOrEqualTo(root.get(LDNMessageEntity_.queueAttempts), max_attempts));
+ andPredicates.add(criteriaBuilder.lessThan(root.get(LDNMessageEntity_.queueTimeout), new Date()));
+ criteriaQuery.where(criteriaBuilder.and(andPredicates.toArray(new Predicate[] {})));
+ List orderList = new LinkedList<>();
+ orderList.add(criteriaBuilder.desc(root.get(LDNMessageEntity_.queueAttempts)));
+ orderList.add(criteriaBuilder.asc(root.get(LDNMessageEntity_.queueLastStartTime)));
+ criteriaQuery.orderBy(orderList);
+ List result = list(context, criteriaQuery, false, LDNMessageEntity.class, -1, -1);
+ if (result == null || result.isEmpty()) {
+ log.debug("No LDN messages found to be processed");
+ }
+ return result;
+ }
+
+ @Override
+ public List findAllRelatedMessagesByItem(
+ Context context, LDNMessageEntity msg, Item item, String... relatedTypes) throws SQLException {
+ CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
+ CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, LDNMessageEntity.class);
+ Root root = criteriaQuery.from(LDNMessageEntity.class);
+ criteriaQuery.select(root);
+ List andPredicates = new ArrayList<>();
+ Predicate relatedtypePredicate = null;
+ andPredicates.add(
+ criteriaBuilder.equal(root.get(LDNMessageEntity_.queueStatus), LDNMessageEntity.QUEUE_STATUS_PROCESSED));
+ andPredicates.add(
+ criteriaBuilder.isNull(root.get(LDNMessageEntity_.target)));
+ andPredicates.add(
+ criteriaBuilder.equal(root.get(LDNMessageEntity_.inReplyTo), msg));
+ if (relatedTypes != null && relatedTypes.length > 0) {
+ relatedtypePredicate = root.get(LDNMessageEntity_.activityStreamType).in(relatedTypes);
+ andPredicates.add(relatedtypePredicate);
+ }
+ criteriaQuery.where(criteriaBuilder.and(andPredicates.toArray(new Predicate[] {})));
+ List orderList = new LinkedList<>();
+ orderList.add(criteriaBuilder.asc(root.get(LDNMessageEntity_.queueLastStartTime)));
+ orderList.add(criteriaBuilder.desc(root.get(LDNMessageEntity_.queueAttempts)));
+ criteriaQuery.orderBy(orderList);
+ List result = list(context, criteriaQuery, false, LDNMessageEntity.class, -1, -1);
+ if (result == null || result.isEmpty()) {
+ log.debug("No LDN messages ACK found to be processed");
+ }
+ return result;
+ }
+
+ @Override
+ public List findAllMessagesByItem(
+ Context context, Item item, String... activities) throws SQLException {
+ CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
+ CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, LDNMessageEntity.class);
+ Root root = criteriaQuery.from(LDNMessageEntity.class);
+ criteriaQuery.select(root);
+ List andPredicates = new ArrayList<>();
+ Predicate activityPredicate = null;
+ andPredicates.add(
+ criteriaBuilder.equal(root.get(LDNMessageEntity_.queueStatus), LDNMessageEntity.QUEUE_STATUS_PROCESSED));
+ andPredicates.add(
+ criteriaBuilder.equal(root.get(LDNMessageEntity_.object), item));
+ if (activities != null && activities.length > 0) {
+ activityPredicate = root.get(LDNMessageEntity_.activityStreamType).in(activities);
+ andPredicates.add(activityPredicate);
+ }
+ criteriaQuery.where(criteriaBuilder.and(andPredicates.toArray(new Predicate[] {})));
+ List orderList = new LinkedList<>();
+ orderList.add(criteriaBuilder.asc(root.get(LDNMessageEntity_.queueLastStartTime)));
+ orderList.add(criteriaBuilder.desc(root.get(LDNMessageEntity_.queueAttempts)));
+ criteriaQuery.orderBy(orderList);
+ List result = list(context, criteriaQuery, false, LDNMessageEntity.class, -1, -1);
+ if (result == null || result.isEmpty()) {
+ log.debug("No LDN messages found");
+ }
+ return result;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/NotifyPatternToTriggerDaoImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/NotifyPatternToTriggerDaoImpl.java
new file mode 100644
index 000000000000..53cbeabe005a
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/NotifyPatternToTriggerDaoImpl.java
@@ -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.app.ldn.dao.impl;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Root;
+import org.dspace.app.ldn.NotifyPatternToTrigger;
+import org.dspace.app.ldn.NotifyPatternToTrigger_;
+import org.dspace.app.ldn.dao.NotifyPatternToTriggerDao;
+import org.dspace.content.Item;
+import org.dspace.core.AbstractHibernateDAO;
+import org.dspace.core.Context;
+
+/**
+ * Implementation of {@link NotifyPatternToTriggerDao}.
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+public class NotifyPatternToTriggerDaoImpl extends AbstractHibernateDAO
+ implements NotifyPatternToTriggerDao {
+
+ @Override
+ public List findByItem(Context context, Item item)
+ throws SQLException {
+ CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
+ CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, NotifyPatternToTrigger.class);
+ Root notifyServiceEntityRoot = criteriaQuery.from(NotifyPatternToTrigger.class);
+ criteriaQuery.select(notifyServiceEntityRoot);
+ criteriaQuery.where(criteriaBuilder.equal(
+ notifyServiceEntityRoot.get(NotifyPatternToTrigger_.item), item));
+ return list(context, criteriaQuery, false, NotifyPatternToTrigger.class, -1, -1);
+ }
+ @Override
+ public List findByItemAndPattern(Context context, Item item, String pattern)
+ throws SQLException {
+ CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
+ CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, NotifyPatternToTrigger.class);
+ Root notifyServiceEntityRoot = criteriaQuery.from(NotifyPatternToTrigger.class);
+ criteriaQuery.select(notifyServiceEntityRoot);
+ criteriaQuery.where(criteriaBuilder.and(
+ criteriaBuilder.equal(
+ notifyServiceEntityRoot.get(NotifyPatternToTrigger_.item), item),
+ criteriaBuilder.equal(
+ notifyServiceEntityRoot.get(NotifyPatternToTrigger_.pattern), pattern)
+ ));
+ return list(context, criteriaQuery, false, NotifyPatternToTrigger.class, -1, -1);
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/NotifyServiceDaoImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/NotifyServiceDaoImpl.java
new file mode 100644
index 000000000000..bb4cf791da27
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/NotifyServiceDaoImpl.java
@@ -0,0 +1,62 @@
+/**
+ * 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.dao.impl;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Join;
+import jakarta.persistence.criteria.Root;
+import org.dspace.app.ldn.NotifyServiceEntity;
+import org.dspace.app.ldn.NotifyServiceEntity_;
+import org.dspace.app.ldn.NotifyServiceInboundPattern;
+import org.dspace.app.ldn.NotifyServiceInboundPattern_;
+import org.dspace.app.ldn.dao.NotifyServiceDao;
+import org.dspace.core.AbstractHibernateDAO;
+import org.dspace.core.Context;
+
+/**
+ * Implementation of {@link NotifyServiceDao}.
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+public class NotifyServiceDaoImpl extends AbstractHibernateDAO implements NotifyServiceDao {
+
+ @Override
+ public NotifyServiceEntity findByLdnUrl(Context context, String ldnUrl) throws SQLException {
+ CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
+ CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, NotifyServiceEntity.class);
+ Root notifyServiceEntityRoot = criteriaQuery.from(NotifyServiceEntity.class);
+ criteriaQuery.select(notifyServiceEntityRoot);
+ criteriaQuery.where(criteriaBuilder.equal(
+ notifyServiceEntityRoot.get(NotifyServiceEntity_.ldnUrl), ldnUrl));
+ return uniqueResult(context, criteriaQuery, false, NotifyServiceEntity.class);
+ }
+
+ @Override
+ public List findManualServicesByInboundPattern(Context context, String pattern)
+ throws SQLException {
+ CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
+ CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, NotifyServiceEntity.class);
+ Root notifyServiceEntityRoot = criteriaQuery.from(NotifyServiceEntity.class);
+
+ Join notifyServiceInboundPatternJoin =
+ notifyServiceEntityRoot.join(NotifyServiceEntity_.inboundPatterns);
+
+ criteriaQuery.select(notifyServiceEntityRoot);
+ criteriaQuery.where(criteriaBuilder.and(
+ criteriaBuilder.equal(
+ notifyServiceInboundPatternJoin.get(NotifyServiceInboundPattern_.pattern), pattern),
+ criteriaBuilder.equal(
+ notifyServiceInboundPatternJoin.get(NotifyServiceInboundPattern_.automatic), false)));
+
+ return list(context, criteriaQuery, false, NotifyServiceEntity.class, -1, -1);
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/NotifyServiceInboundPatternDaoImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/NotifyServiceInboundPatternDaoImpl.java
new file mode 100644
index 000000000000..dc3dc1c74491
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/NotifyServiceInboundPatternDaoImpl.java
@@ -0,0 +1,59 @@
+/**
+ * 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.dao.impl;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Root;
+import org.dspace.app.ldn.NotifyServiceEntity;
+import org.dspace.app.ldn.NotifyServiceInboundPattern;
+import org.dspace.app.ldn.NotifyServiceInboundPattern_;
+import org.dspace.app.ldn.dao.NotifyServiceInboundPatternDao;
+import org.dspace.core.AbstractHibernateDAO;
+import org.dspace.core.Context;
+
+/**
+ * Implementation of {@link NotifyServiceInboundPatternDao}.
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+public class NotifyServiceInboundPatternDaoImpl
+ extends AbstractHibernateDAO implements NotifyServiceInboundPatternDao {
+
+ @Override
+ public NotifyServiceInboundPattern findByServiceAndPattern(Context context, NotifyServiceEntity notifyServiceEntity,
+ String pattern) throws SQLException {
+ CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
+ CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, NotifyServiceInboundPattern.class);
+ Root inboundPatternRoot = criteriaQuery.from(NotifyServiceInboundPattern.class);
+ criteriaQuery.select(inboundPatternRoot);
+ criteriaQuery.where(criteriaBuilder.and(
+ criteriaBuilder.equal(
+ inboundPatternRoot.get(NotifyServiceInboundPattern_.notifyService), notifyServiceEntity),
+ criteriaBuilder.equal(
+ inboundPatternRoot.get(NotifyServiceInboundPattern_.pattern), pattern)
+ ));
+ return uniqueResult(context, criteriaQuery, false, NotifyServiceInboundPattern.class);
+ }
+
+ @Override
+ public List findAutomaticPatterns(Context context) throws SQLException {
+ CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
+ CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, NotifyServiceInboundPattern.class);
+ Root inboundPatternRoot = criteriaQuery.from(NotifyServiceInboundPattern.class);
+ criteriaQuery.select(inboundPatternRoot);
+ criteriaQuery.where(
+ criteriaBuilder.equal(
+ inboundPatternRoot.get(NotifyServiceInboundPattern_.automatic), true)
+ );
+ return list(context, criteriaQuery, false, NotifyServiceInboundPattern.class, -1, -1);
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNMessageServiceFactory.java b/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNMessageServiceFactory.java
new file mode 100644
index 000000000000..bbf521123bca
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNMessageServiceFactory.java
@@ -0,0 +1,29 @@
+/**
+ * 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.factory;
+
+import org.dspace.app.ldn.service.LDNMessageService;
+import org.dspace.services.factory.DSpaceServicesFactory;
+
+/**
+ * Abstract factory to get services for the NotifyService package,
+ * use NotifyServiceFactory.getInstance() to retrieve an implementation
+ *
+ * @author Francesco Bacchelli (francesco.bacchelli at 4science.com)
+ */
+public abstract class LDNMessageServiceFactory {
+
+ public abstract LDNMessageService getLDNMessageService();
+
+ public static LDNMessageServiceFactory getInstance() {
+ return DSpaceServicesFactory.getInstance()
+ .getServiceManager()
+ .getServiceByName("ldnMessageServiceFactory",
+ LDNMessageServiceFactory.class);
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNMessageServiceFactoryImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNMessageServiceFactoryImpl.java
new file mode 100644
index 000000000000..a001ece04069
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNMessageServiceFactoryImpl.java
@@ -0,0 +1,29 @@
+/**
+ * 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.factory;
+
+import org.dspace.app.ldn.service.LDNMessageService;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Factory implementation to get services for the notifyservices package, use
+ * NotifyServiceFactory.getInstance() to retrieve an implementation
+ *
+ * @author Francesco Bacchelli (francesco.bacchelli at 4science.com)
+ */
+public class LDNMessageServiceFactoryImpl extends LDNMessageServiceFactory {
+
+ @Autowired(required = true)
+ private LDNMessageService ldnMessageService;
+
+ @Override
+ public LDNMessageService getLDNMessageService() {
+ return ldnMessageService;
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNRouterFactory.java b/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNRouterFactory.java
new file mode 100644
index 000000000000..4b0f107d2498
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNRouterFactory.java
@@ -0,0 +1,29 @@
+/**
+ * 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.factory;
+
+import org.dspace.app.ldn.LDNRouter;
+import org.dspace.services.factory.DSpaceServicesFactory;
+
+/**
+ * Abstract factory to get services for the ldn package, use
+ * LDNRouterFactory.getInstance() to retrieve an implementation
+ *
+ * @author Francesco Bacchelli (francesco.bacchelli at 4science.com)
+ */
+public abstract class LDNRouterFactory {
+
+ public abstract LDNRouter getLDNRouter();
+
+ public static LDNRouterFactory getInstance() {
+ return DSpaceServicesFactory.getInstance()
+ .getServiceManager()
+ .getServiceByName("ldnRouter",
+ LDNRouterFactory.class);
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNRouterFactoryImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNRouterFactoryImpl.java
new file mode 100644
index 000000000000..f411b9d935d0
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNRouterFactoryImpl.java
@@ -0,0 +1,28 @@
+/**
+ * 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.factory;
+
+import org.dspace.app.ldn.LDNRouter;
+import org.springframework.beans.factory.annotation.Autowired;
+/**
+ * Factory implementation to get services for the ldn package,
+ * use ldnRouter spring bean instance to retrieve an implementation
+ *
+ * @author Francesco Bacchelli (mohamed.eskander at 4science.com)
+ */
+public class LDNRouterFactoryImpl extends LDNRouterFactory {
+
+ @Autowired(required = true)
+ private LDNRouter ldnRouter;
+
+ @Override
+ public LDNRouter getLDNRouter() {
+ return ldnRouter;
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/factory/NotifyServiceFactory.java b/dspace-api/src/main/java/org/dspace/app/ldn/factory/NotifyServiceFactory.java
new file mode 100644
index 000000000000..ea488ca25031
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/factory/NotifyServiceFactory.java
@@ -0,0 +1,36 @@
+/**
+ * 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.factory;
+
+import org.dspace.app.ldn.service.LDNMessageService;
+import org.dspace.app.ldn.service.NotifyPatternToTriggerService;
+import org.dspace.app.ldn.service.NotifyService;
+import org.dspace.app.ldn.service.NotifyServiceInboundPatternService;
+import org.dspace.services.factory.DSpaceServicesFactory;
+
+/**
+ * Abstract factory to get services for the NotifyService package,
+ * use NotifyServiceFactory.getInstance() to retrieve an implementation
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+public abstract class NotifyServiceFactory {
+
+ public abstract NotifyService getNotifyService();
+
+ public abstract NotifyServiceInboundPatternService getNotifyServiceInboundPatternService();
+
+ public abstract NotifyPatternToTriggerService getNotifyPatternToTriggerService();
+
+ public abstract LDNMessageService getLDNMessageService();
+
+ public static NotifyServiceFactory getInstance() {
+ return DSpaceServicesFactory.getInstance().getServiceManager().getServiceByName(
+ "notifyServiceFactory", NotifyServiceFactory.class);
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/factory/NotifyServiceFactoryImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/factory/NotifyServiceFactoryImpl.java
new file mode 100644
index 000000000000..84e15ee261a2
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/factory/NotifyServiceFactoryImpl.java
@@ -0,0 +1,56 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.app.ldn.factory;
+
+import org.dspace.app.ldn.service.LDNMessageService;
+import org.dspace.app.ldn.service.NotifyPatternToTriggerService;
+import org.dspace.app.ldn.service.NotifyService;
+import org.dspace.app.ldn.service.NotifyServiceInboundPatternService;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Factory implementation to get services for the notifyservices package,
+ * use NotifyServiceFactory.getInstance() to retrieve an implementation
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+public class NotifyServiceFactoryImpl extends NotifyServiceFactory {
+
+ @Autowired(required = true)
+ private NotifyService notifyService;
+
+ @Autowired(required = true)
+ private NotifyServiceInboundPatternService notifyServiceInboundPatternService;
+
+ @Autowired(required = true)
+ private NotifyPatternToTriggerService notifyPatternToTriggerService;
+
+ @Autowired(required = true)
+ private LDNMessageService ldnMessageService;
+
+ @Override
+ public NotifyService getNotifyService() {
+ return notifyService;
+ }
+
+ @Override
+ public NotifyServiceInboundPatternService getNotifyServiceInboundPatternService() {
+ return notifyServiceInboundPatternService;
+ }
+
+ @Override
+ public NotifyPatternToTriggerService getNotifyPatternToTriggerService() {
+ return notifyPatternToTriggerService;
+ }
+
+ @Override
+ public LDNMessageService getLDNMessageService() {
+ return ldnMessageService;
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/Actor.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/Actor.java
new file mode 100644
index 000000000000..a81cc3f8008a
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/Actor.java
@@ -0,0 +1,41 @@
+/**
+ * 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.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * used to map @see org.dspace.app.ldn.model.Notification
+ */
+public class Actor extends Base {
+
+ @JsonProperty("name")
+ private String name;
+
+ /**
+ *
+ */
+ public Actor() {
+ super();
+ }
+
+ /**
+ * @return String
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @param name
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/Base.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/Base.java
new file mode 100644
index 000000000000..6ddaae110e1f
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/Base.java
@@ -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.app.ldn.model;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+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)
+public class Base {
+
+ @JsonProperty("id")
+ private String id;
+
+ @JsonProperty("type")
+ private Set type;
+
+ /**
+ *
+ */
+ public Base() {
+ type = new HashSet<>();
+ }
+
+ /**
+ * @return String
+ */
+ public String getId() {
+ return id;
+ }
+
+ /**
+ * @param id
+ */
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ /**
+ * @return Set
+ */
+ public Set getType() {
+ return type;
+ }
+
+ /**
+ * @param type
+ */
+ public void setType(java.lang.Object type) {
+ if (type instanceof String) {
+ this.type.add((String) type);
+ } else if (type instanceof Collection) {
+ this.type.addAll((Collection) type);
+ }
+ }
+
+ /**
+ * @param type
+ */
+ public void addType(String type) {
+ this.type.add(type);
+ }
+
+ /**
+ * @return int
+ */
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((id == null) ? 0 : id.hashCode());
+ return result;
+ }
+
+ /**
+ * @param obj
+ * @return boolean
+ */
+ @Override
+ public boolean equals(java.lang.Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Base other = (Base) obj;
+ if (id == null) {
+ if (other.id != null) {
+ return false;
+ }
+ } else if (!id.equals(other.id)) {
+ return false;
+ }
+ return true;
+ }
+
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/Citation.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/Citation.java
new file mode 100644
index 000000000000..7abe5c8ef44a
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/Citation.java
@@ -0,0 +1,59 @@
+/**
+ * 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.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+
+/**
+ * used to map @see org.dspace.app.ldn.model.Notification
+ */
+public class Citation extends Base {
+
+ @JsonProperty("ietf:cite-as")
+ private String ietfCiteAs;
+
+ @JsonProperty("ietf:item")
+ private Url url;
+
+ /**
+ *
+ */
+ public Citation() {
+ super();
+ }
+
+ /**
+ * @return String
+ */
+ public String getIetfCiteAs() {
+ return ietfCiteAs;
+ }
+
+ /**
+ * @param ietfCiteAs
+ */
+ public void setIetfCiteAs(String ietfCiteAs) {
+ this.ietfCiteAs = ietfCiteAs;
+ }
+
+ /**
+ * @return Url
+ */
+ public Url getUrl() {
+ return url;
+ }
+
+ /**
+ * @param url
+ */
+ public void setUrl(Url url) {
+ this.url = url;
+ }
+
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/Context.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/Context.java
new file mode 100644
index 000000000000..78fe37341697
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/Context.java
@@ -0,0 +1,61 @@
+/**
+ * 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.model;
+
+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 {
+
+ @JsonProperty("IsSupplementedBy")
+ private List isSupplementedBy;
+
+ @JsonProperty("IsSupplementTo")
+ private List isSupplementTo;
+
+ /**
+ *
+ */
+ public Context() {
+ super();
+ }
+
+ /**
+ * @return List
+ */
+ public List getIsSupplementedBy() {
+ return isSupplementedBy;
+ }
+
+ /**
+ * @param isSupplementedBy
+ */
+ public void setIsSupplementedBy(List isSupplementedBy) {
+ this.isSupplementedBy = isSupplementedBy;
+ }
+
+ /**
+ * @return List
+ */
+ public List getIsSupplementTo() {
+ return isSupplementTo;
+ }
+
+ /**
+ * @param isSupplementTo
+ */
+ public void setIsSupplementTo(List isSupplementTo) {
+ this.isSupplementTo = isSupplementTo;
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/Notification.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/Notification.java
new file mode 100644
index 000000000000..52bc9840f42d
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/Notification.java
@@ -0,0 +1,159 @@
+/**
+ * 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.model;
+
+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.
+ * see official coar doc
+ */
+@JsonPropertyOrder(value = {
+ "@context",
+ "id",
+ "type",
+ "actor",
+ "context",
+ "object",
+ "origin",
+ "target",
+ "inReplyTo"
+})
+public class Notification extends Base {
+
+ @JsonProperty("@context")
+ private String[] c = new String[] {
+ "https://purl.org/coar/notify",
+ "https://www.w3.org/ns/activitystreams"
+ };
+
+ @JsonProperty("actor")
+ private Actor actor;
+
+ @JsonProperty("context")
+ private Context context;
+
+ @JsonProperty("object")
+ private Object object;
+
+ @JsonProperty("origin")
+ private Service origin;
+
+ @JsonProperty("target")
+ private Service target;
+
+ @JsonProperty("inReplyTo")
+ private String inReplyTo;
+
+ /**
+ *
+ */
+ public Notification() {
+ super();
+ }
+
+ /**
+ * @return String[]
+ */
+ public String[] getC() {
+ return c;
+ }
+
+ /**
+ * @param c
+ */
+ public void setC(String[] c) {
+ this.c = c;
+ }
+
+ /**
+ * @return Actor
+ */
+ public Actor getActor() {
+ return actor;
+ }
+
+ /**
+ * @param actor
+ */
+ public void setActor(Actor actor) {
+ this.actor = actor;
+ }
+
+ /**
+ * @return Context
+ */
+ public Context getContext() {
+ return context;
+ }
+
+ /**
+ * @param context
+ */
+ public void setContext(Context context) {
+ this.context = context;
+ }
+
+ /**
+ * @return Object
+ */
+ public Object getObject() {
+ return object;
+ }
+
+ /**
+ * @param object
+ */
+ public void setObject(Object object) {
+ this.object = object;
+ }
+
+ /**
+ * @return Service
+ */
+ public Service getOrigin() {
+ return origin;
+ }
+
+ /**
+ * @param origin
+ */
+ public void setOrigin(Service origin) {
+ this.origin = origin;
+ }
+
+ /**
+ * @return Service
+ */
+ public Service getTarget() {
+ return target;
+ }
+
+ /**
+ * @param target
+ */
+ public void setTarget(Service target) {
+ this.target = target;
+ }
+
+ /**
+ * @return String
+ */
+ public String getInReplyTo() {
+ return inReplyTo;
+ }
+
+ /**
+ * @param inReplyTo
+ */
+ public void setInReplyTo(String inReplyTo) {
+ this.inReplyTo = inReplyTo;
+ }
+
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/NotifyRequestStatus.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/NotifyRequestStatus.java
new file mode 100644
index 000000000000..0302b528aa8d
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/NotifyRequestStatus.java
@@ -0,0 +1,63 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.app.ldn.model;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+
+@JsonPropertyOrder(value = {
+ "itemuuid",
+ "notifyStatus"
+})
+
+/**
+ * item requests of LDN messages of type
+ *
+ * "Offer", "coar-notify:EndorsementAction"
+ * "Offer", "coar-notify:IngestAction"
+ * "Offer", "coar-notify:ReviewAction"
+ *
+ * and their acknownledgements - if any
+ *
+ * @author Francesco Bacchelli (francesco.bacchelli at 4science dot it)
+ */
+public class NotifyRequestStatus extends Base {
+
+ private UUID itemUuid;
+
+ private List notifyStatus;
+
+ public NotifyRequestStatus() {
+ super();
+ this.notifyStatus = new ArrayList();
+ }
+
+ public UUID getItemUuid() {
+ return itemUuid;
+ }
+
+ public void setItemUuid(UUID itemUuid) {
+ this.itemUuid = itemUuid;
+ }
+
+ public void addRequestStatus(RequestStatus rs) {
+ this.notifyStatus.add(rs);
+ }
+
+ public List getNotifyStatus() {
+ return notifyStatus;
+ }
+
+ public void setNotifyStatus(List notifyStatus) {
+ this.notifyStatus = notifyStatus;
+ }
+
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/NotifyRequestStatusEnum.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/NotifyRequestStatusEnum.java
new file mode 100644
index 000000000000..437c624f84d8
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/NotifyRequestStatusEnum.java
@@ -0,0 +1,18 @@
+/**
+ * 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.model;
+/**
+ * REQUESTED means acknowledgements not received yet
+ * ACCEPTED means acknowledgements of "Accept" type received
+ * REJECTED means ack of "TentativeReject" type received
+ *
+ * @author Francesco Bacchelli (francesco.bacchelli at 4science.com)
+ */
+public enum NotifyRequestStatusEnum {
+ REJECTED, ACCEPTED, REQUESTED
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/Object.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/Object.java
new file mode 100644
index 000000000000..8913af47dae1
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/Object.java
@@ -0,0 +1,74 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.app.ldn.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * used to map @see org.dspace.app.ldn.model.Notification
+ */
+public class Object extends Citation {
+
+ @JsonProperty("as:object")
+ private String asObject;
+
+ @JsonProperty("as:relationship")
+ private String asRelationship;
+
+ @JsonProperty("as:subject")
+ private String asSubject;
+
+ @JsonProperty("sorg:name")
+ private String title;
+
+ /**
+ *
+ */
+ public Object() {
+ super();
+ }
+
+ /**
+ * @return String
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * @param title
+ */
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getAsObject() {
+ return asObject;
+ }
+
+ public void setAsObject(String asObject) {
+ this.asObject = asObject;
+ }
+
+ public String getAsRelationship() {
+ return asRelationship;
+ }
+
+ public void setAsRelationship(String asRelationship) {
+ this.asRelationship = asRelationship;
+ }
+
+ public String getAsSubject() {
+ return asSubject;
+ }
+
+ public void setAsSubject(String asSubject) {
+ this.asSubject = asSubject;
+ }
+
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/RequestStatus.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/RequestStatus.java
new file mode 100644
index 000000000000..d19369830787
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/RequestStatus.java
@@ -0,0 +1,47 @@
+/**
+ * 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.model;
+
+/**
+ * Informations about the Offer and Acknowledgements targeting a specified Item
+ *
+ * @author Francesco Bacchelli (francesco.bacchelli at 4science.com)
+ */
+public class RequestStatus {
+
+ private String serviceName;
+ private String serviceUrl;
+ private String offerType;
+ private NotifyRequestStatusEnum status;
+
+ public String getServiceName() {
+ return serviceName;
+ }
+ public void setServiceName(String serviceName) {
+ this.serviceName = serviceName;
+ }
+ public String getServiceUrl() {
+ return serviceUrl;
+ }
+ public void setServiceUrl(String serviceUrl) {
+ this.serviceUrl = serviceUrl;
+ }
+ public NotifyRequestStatusEnum getStatus() {
+ return status;
+ }
+ public void setStatus(NotifyRequestStatusEnum status) {
+ this.status = status;
+ }
+ public String getOfferType() {
+ return offerType;
+ }
+ public void setOfferType(String offerType) {
+ this.offerType = offerType;
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/Service.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/Service.java
new file mode 100644
index 000000000000..cdd3ba5bb536
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/Service.java
@@ -0,0 +1,41 @@
+/**
+ * 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.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * used to map @see org.dspace.app.ldn.model.Notification
+ */
+public class Service extends Base {
+
+ @JsonProperty("inbox")
+ private String inbox;
+
+ /**
+ *
+ */
+ public Service() {
+ super();
+ }
+
+ /**
+ * @return String
+ */
+ public String getInbox() {
+ return inbox;
+ }
+
+ /**
+ * @param inbox
+ */
+ public void setInbox(String inbox) {
+ this.inbox = inbox;
+ }
+
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/Url.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/Url.java
new file mode 100644
index 000000000000..47093e02e44b
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/Url.java
@@ -0,0 +1,41 @@
+/**
+ * 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.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * used to map @see org.dspace.app.ldn.model.Citation
+ */
+public class Url extends Base {
+
+ @JsonProperty("mediaType")
+ private String mediaType;
+
+ /**
+ *
+ */
+ public Url() {
+ super();
+ }
+
+ /**
+ * @return String
+ */
+ public String getMediaType() {
+ return mediaType;
+ }
+
+ /**
+ * @param mediaType
+ */
+ public void setMediaType(String mediaType) {
+ this.mediaType = mediaType;
+ }
+
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNContextRepeater.java b/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNContextRepeater.java
new file mode 100644
index 000000000000..a5ef0957ff8f
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNContextRepeater.java
@@ -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.app.ldn.processor;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.dspace.app.ldn.model.Context;
+import org.dspace.app.ldn.model.Notification;
+
+/**
+ * Context repeater to iterate over array context properties of a received
+ * notification. The returned notification iterator is a notification with
+ * context array elements hoisted onto the root of the notification context.
+ */
+public class LDNContextRepeater {
+
+ private final static Logger log = LogManager.getLogger(LDNContextRepeater.class);
+
+ private final static String CONTEXT = "context";
+
+ private String repeatOver;
+
+ /**
+ * @return String
+ */
+ public String getRepeatOver() {
+ return repeatOver;
+ }
+
+ /**
+ * @param repeatOver
+ */
+ public void setRepeatOver(String repeatOver) {
+ this.repeatOver = repeatOver;
+ }
+
+ /**
+ * @param notification
+ * @return Iterator
+ */
+ public Iterator iterator(Notification notification) {
+ return new NotificationIterator(notification, repeatOver);
+ }
+
+ /**
+ * Private inner class defining the notification iterator.
+ */
+ private class NotificationIterator implements Iterator {
+
+ private final List notifications;
+
+ /**
+ * Convert notification to JsonNode in order to clone for each context array
+ * element. Each element is then hoisted to the root of the cloned notification
+ * context.
+ *
+ * @param notification received notification
+ * @param repeatOver which context property to repeat over
+ */
+ private NotificationIterator(Notification notification, String repeatOver) {
+ this.notifications = new ArrayList<>();
+
+ if (Objects.nonNull(repeatOver)) {
+ ObjectMapper objectMapper = new ObjectMapper();
+
+ JsonNode notificationNode = objectMapper.valueToTree(notification);
+
+ log.debug("Notification {}", notificationNode);
+
+ JsonNode topContextNode = notificationNode.get(CONTEXT);
+ if (topContextNode.isNull()) {
+ log.warn("Notification is missing context");
+ return;
+ }
+
+ JsonNode contextArrayNode = topContextNode.get(repeatOver);
+ if (contextArrayNode == null || contextArrayNode.isNull()) {
+ log.error("Notification context {} is not defined", repeatOver);
+ return;
+ }
+
+ if (contextArrayNode.isArray()) {
+
+ for (JsonNode contextNode : ((ArrayNode) contextArrayNode)) {
+
+ try {
+ Context context = objectMapper.treeToValue(contextNode, Context.class);
+
+ Notification copy = objectMapper.treeToValue(notificationNode, Notification.class);
+
+ copy.setContext(context);
+
+ this.notifications.add(copy);
+ } catch (JsonProcessingException e) {
+ log.error("Failed to copy notification");
+ }
+
+ }
+
+ } else {
+ log.error("Notification context {} is not an array", repeatOver);
+ }
+
+ } else {
+ this.notifications.add(notification);
+ }
+ }
+
+ /**
+ * @return boolean
+ */
+ @Override
+ public boolean hasNext() {
+ return !this.notifications.isEmpty();
+ }
+
+ /**
+ * @return Notification
+ */
+ @Override
+ public Notification next() {
+ return this.notifications.remove(0);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNMetadataProcessor.java b/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNMetadataProcessor.java
new file mode 100644
index 000000000000..43c50173ee61
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNMetadataProcessor.java
@@ -0,0 +1,231 @@
+/**
+ * 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.processor;
+
+import static java.lang.String.format;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+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.LDNAction;
+import org.dspace.app.ldn.action.LDNActionStatus;
+import org.dspace.app.ldn.model.Notification;
+import org.dspace.app.ldn.service.LDNMessageService;
+import org.dspace.app.ldn.utility.LDNUtils;
+import org.dspace.content.DSpaceObject;
+import org.dspace.content.Item;
+import org.dspace.content.service.ItemService;
+import org.dspace.core.Constants;
+import org.dspace.core.Context;
+import org.dspace.handle.service.HandleService;
+import org.dspace.services.ConfigurationService;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Linked Data Notification metadata processor for consuming notifications. The
+ * storage of notification details are within item metadata.
+ */
+public class LDNMetadataProcessor implements LDNProcessor {
+
+ private final static Logger log = LogManager.getLogger(LDNMetadataProcessor.class);
+
+ @Autowired
+ private ItemService itemService;
+
+ @Autowired
+ private LDNMessageService ldnMessageService;
+
+ @Autowired
+ private ConfigurationService configurationService;
+
+ private static final Set OBJECT_SUBJECT_ITEM_TYPES = Set.of(
+ "Announce",
+ "coar-notify:RelationshipAction");
+
+ private static final Set CONTEXT_ID_ITEM_TYPES = Set.of(
+ "Announce",
+ "TentativeReject",
+ "Accept",
+ "coar-notify:ReviewAction",
+ "coar-notify:IngestAction",
+ "coar-notify:EndorsementAction");
+
+ private static final Set OBJECT_ID_ITEM_TYPES = Set.of(
+ "Offer",
+ "coar-notify:ReviewAction",
+ "coar-notify:EndorsementAction",
+ "coar-notify:IngestAction");
+
+ @Autowired
+ private HandleService handleService;
+
+ private LDNContextRepeater repeater = new LDNContextRepeater();
+
+ private List actions = new ArrayList<>();
+
+ /**
+ * Initialize velocity engine for templating.
+ */
+ private LDNMetadataProcessor() {
+
+ }
+
+ /**
+ * Process notification by repeating over context, processing each context
+ * notification, and running actions post processing.
+ *
+ * @param notification received notification
+ * @throws Exception something went wrong processing the notification
+ */
+ @Override
+ public void process(Context context, Notification notification) throws Exception {
+ Item item = lookupItem(context, notification);
+ runActions(context, notification, item);
+ }
+
+ /**
+ * Run all actions defined for the processor.
+ *
+ * @param notification current context notification
+ * @param item associated item
+ *
+ * @return ActionStatus result status of running the action
+ *
+ * @throws Exception failed execute the action
+ */
+ 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(),
+ notification.getId(),
+ notification.getType());
+
+ operation = action.execute(context, notification, item);
+ if (operation == LDNActionStatus.ABORT) {
+ break;
+ }
+ }
+
+ return operation;
+ }
+
+ /**
+ * @return LDNContextRepeater
+ */
+ public LDNContextRepeater getRepeater() {
+ return repeater;
+ }
+
+ /**
+ * @param repeater
+ */
+ public void setRepeater(LDNContextRepeater repeater) {
+ this.repeater = repeater;
+ }
+
+ /**
+ * @return List
+ */
+ public List getActions() {
+ return actions;
+ }
+
+ /**
+ * @param actions
+ */
+ public void setActions(List actions) {
+ this.actions = actions;
+ }
+
+ /**
+ * Lookup associated item to the notification context. If UUID in URL, lookup bu
+ * UUID, else lookup by handle.
+ *
+ * @param context current context
+ * @param notification current context notification
+ *
+ * @return Item associated item
+ *
+ * @throws SQLException failed to lookup item
+ * @throws HttpResponseException redirect failure
+ */
+ private Item lookupItem(Context context, Notification notification) throws SQLException, HttpResponseException {
+ Item item = null;
+ String url = null;
+
+ if (CONTEXT_ID_ITEM_TYPES.containsAll(notification.getType())) {
+ url = notification.getContext().getId();
+ } else if (OBJECT_ID_ITEM_TYPES.containsAll(notification.getType())) {
+ url = notification.getObject().getId();
+ } else if (OBJECT_SUBJECT_ITEM_TYPES.containsAll(notification.getType())) {
+ // need to understand if we're sender or receiver
+ if (ldnMessageService.isTargetCurrent(notification)) {
+ // this means we're sending the notification
+ url = notification.getObject().getAsObject();
+ // use as:object for sender
+ } else {
+ // this means we're receiving the notification
+ url = notification.getObject().getAsSubject();
+ // use as:subject for receiver
+ }
+ }
+
+ log.info("Looking up item {}", url);
+
+ item = resolveItemByUrl(context, url, notification);
+
+ return item;
+ }
+
+ private Item resolveItemByUrl(Context context, String url, Notification notification)
+ throws SQLException, HttpResponseException {
+ Item item = null;
+ if (LDNUtils.hasUUIDInURL(url)) {
+ UUID uuid = LDNUtils.getUUIDFromURL(url);
+
+ item = itemService.find(context, uuid);
+
+ if (Objects.isNull(item)) {
+ throw new HttpResponseException(HttpStatus.SC_NOT_FOUND,
+ format("Item with uuid %s not found", uuid));
+ }
+ return item;
+ }
+ String handle = handleService.resolveUrlToHandle(context, url);
+
+ if (Objects.isNull(handle)) {
+ 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 HttpResponseException(HttpStatus.SC_NOT_FOUND,
+ format("Item with handle %s not found", handle));
+ }
+
+ if (object.getType() == Constants.ITEM) {
+ item = (Item) object;
+ } else {
+ throw new HttpResponseException(HttpStatus.SC_UNPROCESSABLE_ENTITY,
+ format("Handle %s does not resolve to an item", handle));
+ }
+ return item;
+ }
+
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNProcessor.java b/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNProcessor.java
new file mode 100644
index 000000000000..279ec5cedc4b
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNProcessor.java
@@ -0,0 +1,25 @@
+/**
+ * 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.processor;
+
+import org.dspace.app.ldn.model.Notification;
+import org.dspace.core.Context;
+
+/**
+ * Processor interface to allow for custom implementations of process.
+ */
+public interface LDNProcessor {
+
+ /**
+ * Process received notification.
+ *
+ * @param notification received notification
+ * @throws Exception something went wrong processing the notification
+ */
+ public void process(Context context, Notification notification) throws Exception;
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/service/LDNMessageService.java b/dspace-api/src/main/java/org/dspace/app/ldn/service/LDNMessageService.java
new file mode 100644
index 000000000000..eb18c6a69a70
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/service/LDNMessageService.java
@@ -0,0 +1,165 @@
+/**
+ * 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.service;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import org.dspace.app.ldn.LDNMessageEntity;
+import org.dspace.app.ldn.NotifyServiceEntity;
+import org.dspace.app.ldn.model.Notification;
+import org.dspace.app.ldn.model.NotifyRequestStatus;
+import org.dspace.app.ldn.model.Service;
+import org.dspace.content.Item;
+import org.dspace.core.Context;
+
+/**
+ * Service interface class for the {@link LDNMessageEntity} object.
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science dot it)
+ */
+public interface LDNMessageService {
+
+ /**
+ * find the ldn message by id
+ *
+ * @param context the context
+ * @param id the uri
+ * @return the ldn message by id
+ * @throws SQLException If something goes wrong in the database
+ */
+ public LDNMessageEntity find(Context context, String id) throws SQLException;
+
+ /**
+ * find all ldn messages
+ *
+ * @param context the context
+ * @return all ldn messages by id
+ * @throws SQLException If something goes wrong in the database
+ */
+ public List findAll(Context context) throws SQLException;
+
+ /**
+ * Creates a new LDNMessage
+ *
+ * @param context The DSpace context
+ * @param id the uri
+ * @return the created LDN Message
+ * @throws SQLException If something goes wrong in the database
+ */
+ public LDNMessageEntity create(Context context, String id) throws SQLException;
+
+ /**
+ * Creates a new LDNMessage
+ *
+ * @param context The DSpace context
+ * @param notification the requested notification
+ * @param sourceIp the source ip
+ * @return the created LDN Message
+ * @throws SQLException If something goes wrong in the database
+ */
+ public LDNMessageEntity create(Context context, Notification notification, String sourceIp) throws SQLException;
+
+ /**
+ * Update the provided LDNMessage
+ *
+ * @param context The DSpace context
+ * @param ldnMessage the LDNMessage
+ * @throws SQLException If something goes wrong in the database
+ */
+ public void update(Context context, LDNMessageEntity ldnMessage) throws SQLException;
+
+ /**
+ * Find the oldest queued LDNMessages that still can be elaborated
+ *
+ * @return list of LDN messages
+ * @param context The DSpace context
+ * @throws SQLException If something goes wrong in the database
+ */
+ public List findOldestMessagesToProcess(Context context) throws SQLException;
+
+ /**
+ * Find all messages in the queue with the Processing status but timed-out
+ *
+ * @return all the LDN Messages to be fixed on their queue_ attributes
+ * @param context The DSpace context
+ * @throws SQLException If something goes wrong in the database
+ */
+ public List findProcessingTimedoutMessages(Context context) throws SQLException;
+
+ /**
+ * Find all messages in the queue with the Processing status but timed-out and modify their queue_status
+ * considering the queue_attempts
+ *
+ * @return number of messages fixed
+ * @param context The DSpace context
+ * @throws SQLException
+ */
+ public int checkQueueMessageTimeout(Context context) throws SQLException;
+
+ /**
+ * Elaborates the oldest enqueued message
+ *
+ * @return number of messages fixed
+ * @param context The DSpace context
+ */
+ public int extractAndProcessMessageFromQueue(Context context) throws SQLException;
+
+ /**
+ * find the related notify service entity
+ *
+ * @param context the context
+ * @param service the service
+ * @return the NotifyServiceEntity
+ * @throws SQLException if something goes wrong
+ */
+ public NotifyServiceEntity findNotifyService(Context context, Service service) throws SQLException;
+
+ /**
+ * find the ldn messages of Requests by item uuid
+ *
+ * @param context the context
+ * @param item the item
+ * @return the item requests object
+ * @throws SQLException If something goes wrong in the database
+ */
+ public NotifyRequestStatus findRequestsByItem(Context context, Item item) throws SQLException;
+
+ /**
+ * delete the provided ldn message
+ *
+ * @param context the context
+ * @param ldnMessage the ldn message
+ * @throws SQLException if something goes wrong
+ */
+ public void delete(Context context, LDNMessageEntity ldnMessage) throws SQLException;
+
+ /**
+ * find the ldn messages to be reprocessed
+ *
+ * @param context the context
+ * @throws SQLException if something goes wrong
+ */
+ public List 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);
+
+ /**
+ * check if the notification is targeting the current system
+ *
+ * @param notification the LDN Message entity
+ */
+ boolean isTargetCurrent(Notification notification);
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/service/NotifyPatternToTriggerService.java b/dspace-api/src/main/java/org/dspace/app/ldn/service/NotifyPatternToTriggerService.java
new file mode 100644
index 000000000000..c2c3de7e06d5
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/service/NotifyPatternToTriggerService.java
@@ -0,0 +1,84 @@
+/**
+ * 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.service;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import org.dspace.app.ldn.NotifyPatternToTrigger;
+import org.dspace.content.Item;
+import org.dspace.core.Context;
+
+/**
+ * Service interface class for the {@link NotifyPatternToTrigger} object.
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+public interface NotifyPatternToTriggerService {
+
+ /**
+ * find all notify patterns to be triggered
+ *
+ * @param context the context
+ * @return all notify patterns to be trigger
+ * @throws SQLException if database error
+ */
+ public List findAll(Context context) throws SQLException;
+
+ /**
+ * find list of Notify Patterns To be Triggered by item
+ *
+ * @param context the context
+ * @param item the item of NotifyPatternToTrigger
+ * @return the matched NotifyPatternToTrigger list by item
+ * @throws SQLException if database error
+ */
+ public List findByItem(Context context, Item item)
+ throws SQLException;
+
+ /**
+ * find list of Notify Patterns To be Triggered by item and pattern
+ *
+ * @param context the context
+ * @param item the item of NotifyPatternToTrigger
+ * @param pattern the pattern of NotifyPatternToTrigger
+ *
+ * @return the matched NotifyPatternToTrigger list by item and pattern
+ * @throws SQLException if database error
+ */
+ public List findByItemAndPattern(Context context, Item item, String pattern)
+ throws SQLException;
+
+ /**
+ * create new notifyPatternToTrigger
+ *
+ * @param context the context
+ * @return the created NotifyPatternToTrigger
+ * @throws SQLException if database error
+ */
+ public NotifyPatternToTrigger create(Context context) throws SQLException;
+
+ /**
+ * update the provided notifyPatternToTrigger
+ *
+ * @param context the context
+ * @param notifyPatternToTrigger the notifyPatternToTrigger
+ * @throws SQLException if database error
+ */
+ public void update(Context context, NotifyPatternToTrigger notifyPatternToTrigger) throws SQLException;
+
+ /**
+ * delete the provided notifyPatternToTrigger
+ *
+ * @param context the context
+ * @param notifyPatternToTrigger the notifyPatternToTrigger
+ * @throws SQLException if database error
+ */
+ public void delete(Context context, NotifyPatternToTrigger notifyPatternToTrigger) throws SQLException;
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/service/NotifyService.java b/dspace-api/src/main/java/org/dspace/app/ldn/service/NotifyService.java
new file mode 100644
index 000000000000..e6ac0d63b438
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/service/NotifyService.java
@@ -0,0 +1,92 @@
+/**
+ * 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.service;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import org.dspace.app.ldn.NotifyServiceEntity;
+import org.dspace.core.Context;
+
+/**
+ * Service interface class for the {@link NotifyServiceEntity} object.
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+public interface NotifyService {
+
+ /**
+ * find all notify service entities
+ *
+ * @param context the context
+ * @return all notify service entities
+ * @throws SQLException if database error
+ */
+ public List findAll(Context context) throws SQLException;
+
+ /**
+ * find one NotifyServiceEntity by id
+ *
+ * @param context the context
+ * @param id the id of NotifyServiceEntity
+ * @return the matched NotifyServiceEntity by id
+ * @throws SQLException if database error
+ */
+ public NotifyServiceEntity find(Context context, Integer id) throws SQLException;
+
+ /**
+ * create new notifyServiceEntity
+ *
+ * @param context the context
+ * @param name name of the service
+ * @return the created NotifyServiceEntity
+ * @throws SQLException if database error
+ */
+ public NotifyServiceEntity create(Context context, String name) throws SQLException;
+
+ /**
+ * update the provided notifyServiceEntity
+ *
+ * @param context the context
+ * @param notifyServiceEntity the notifyServiceEntity
+ * @throws SQLException if database error
+ */
+ public void update(Context context, NotifyServiceEntity notifyServiceEntity) throws SQLException;
+
+ /**
+ * delete the provided notifyServiceEntity
+ *
+ * @param context the context
+ * @param notifyServiceEntity the notifyServiceEntity
+ * @throws SQLException if database error
+ */
+ public void delete(Context context, NotifyServiceEntity notifyServiceEntity) throws SQLException;
+
+ /**
+ * find the NotifyServiceEntity matched with the provided ldnUrl
+ *
+ * @param context the context
+ * @param ldnUrl the ldnUrl
+ * @return the NotifyServiceEntity matched the provided ldnUrl
+ * @throws SQLException if database error
+ */
+ public NotifyServiceEntity findByLdnUrl(Context context, String ldnUrl) throws SQLException;
+
+ /**
+ * find all NotifyServiceEntity matched the provided inbound pattern
+ * from its related notifyServiceInboundPatterns
+ * also with 'automatic' equals to false
+ *
+ * @param context the context
+ * @param pattern the ldnUrl
+ * @return all NotifyServiceEntity matched the provided pattern
+ * @throws SQLException if database error
+ */
+ public List findManualServicesByInboundPattern(Context context, String pattern)
+ throws SQLException;
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/service/NotifyServiceInboundPatternService.java b/dspace-api/src/main/java/org/dspace/app/ldn/service/NotifyServiceInboundPatternService.java
new file mode 100644
index 000000000000..8cd92d45dd30
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/service/NotifyServiceInboundPatternService.java
@@ -0,0 +1,76 @@
+/**
+ * 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.service;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import org.dspace.app.ldn.NotifyServiceEntity;
+import org.dspace.app.ldn.NotifyServiceInboundPattern;
+import org.dspace.core.Context;
+
+/**
+ * Service interface class for the {@link NotifyServiceInboundPattern} object.
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+public interface NotifyServiceInboundPatternService {
+
+ /**
+ * find all notifyServiceInboundPatterns matched with
+ * the provided notifyServiceEntity and pattern
+ *
+ * @param context the context
+ * @param notifyServiceEntity the notifyServiceEntity
+ * @param pattern the pattern
+ * @return all notifyServiceInboundPatterns matched with
+ * the provided notifyServiceEntity and pattern
+ * @throws SQLException if database error
+ */
+ public NotifyServiceInboundPattern findByServiceAndPattern(Context context,
+ NotifyServiceEntity notifyServiceEntity,
+ String pattern) throws SQLException;
+
+ /**
+ * find all automatic notifyServiceInboundPatterns
+ *
+ * @param context the context
+ * @return all automatic notifyServiceInboundPatterns
+ * @throws SQLException if database error
+ */
+ public List findAutomaticPatterns(Context context) throws SQLException;
+
+ /**
+ * create new notifyServiceInboundPattern
+ *
+ * @param context the context
+ * @param notifyServiceEntity the notifyServiceEntity
+ * @return the created notifyServiceInboundPattern
+ * @throws SQLException if database error
+ */
+ public NotifyServiceInboundPattern create(Context context, NotifyServiceEntity notifyServiceEntity)
+ throws SQLException;
+
+ /**
+ * update the provided notifyServiceInboundPattern
+ *
+ * @param context the context
+ * @param inboundPattern the notifyServiceInboundPattern
+ * @throws SQLException if database error
+ */
+ public void update(Context context, NotifyServiceInboundPattern inboundPattern) throws SQLException;
+
+ /**
+ * delete the provided notifyServiceInboundPattern
+ *
+ * @param context the context
+ * @param inboundPattern the notifyServiceInboundPattern
+ * @throws SQLException if database error
+ */
+ public void delete(Context context, NotifyServiceInboundPattern inboundPattern) throws SQLException;
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/LDNMessageServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/LDNMessageServiceImpl.java
new file mode 100644
index 000000000000..15f07a556112
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/LDNMessageServiceImpl.java
@@ -0,0 +1,404 @@
+/**
+ * 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.service.impl;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+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;
+import org.dspace.app.ldn.LDNMessageEntity;
+import org.dspace.app.ldn.LDNRouter;
+import org.dspace.app.ldn.NotifyServiceEntity;
+import org.dspace.app.ldn.dao.LDNMessageDao;
+import org.dspace.app.ldn.dao.NotifyServiceDao;
+import org.dspace.app.ldn.model.Notification;
+import org.dspace.app.ldn.model.NotifyRequestStatus;
+import org.dspace.app.ldn.model.NotifyRequestStatusEnum;
+import org.dspace.app.ldn.model.RequestStatus;
+import org.dspace.app.ldn.model.Service;
+import org.dspace.app.ldn.processor.LDNProcessor;
+import org.dspace.app.ldn.service.LDNMessageService;
+import org.dspace.app.ldn.utility.LDNUtils;
+import org.dspace.content.DSpaceObject;
+import org.dspace.content.Item;
+import org.dspace.content.service.ItemService;
+import org.dspace.core.Constants;
+import org.dspace.core.Context;
+import org.dspace.discovery.indexobject.IndexableLDNNotification;
+import org.dspace.event.Event;
+import org.dspace.handle.service.HandleService;
+import org.dspace.services.ConfigurationService;
+import org.springframework.beans.factory.annotation.Autowired;
+
+
+/**
+ * Implementation of {@link LDNMessageService}
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science dot it)
+ */
+public class LDNMessageServiceImpl implements LDNMessageService {
+
+ @Autowired(required = true)
+ private LDNMessageDao ldnMessageDao;
+ @Autowired(required = true)
+ private NotifyServiceDao notifyServiceDao;
+ @Autowired(required = true)
+ private ConfigurationService configurationService;
+ @Autowired(required = true)
+ private HandleService handleService;
+ @Autowired(required = true)
+ private ItemService itemService;
+ @Autowired(required = true)
+ private LDNRouter ldnRouter;
+
+ private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(LDNMessageServiceImpl.class);
+ private static final String LDN_ID_PREFIX = "urn:uuid:";
+
+ protected LDNMessageServiceImpl() {
+
+ }
+
+ @Override
+ public LDNMessageEntity find(Context context, String id) throws SQLException {
+
+ if (id == null) {
+ return null;
+ }
+
+ id = id.startsWith(LDN_ID_PREFIX) ? id : LDN_ID_PREFIX + id;
+ return ldnMessageDao.findByID(context, LDNMessageEntity.class, id);
+ }
+
+ @Override
+ public List findAll(Context context) throws SQLException {
+ return ldnMessageDao.findAll(context, LDNMessageEntity.class);
+ }
+
+ @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));
+ }
+
+ @Override
+ public LDNMessageEntity create(Context context, Notification notification, String sourceIp) throws SQLException {
+ LDNMessageEntity ldnMessage = create(context, notification.getId());
+ DSpaceObject obj = findDspaceObjectByUrl(context, notification.getObject().getId());
+ if (obj == null) {
+ if (isTargetCurrent(notification)) {
+ // this means we're sending the notification
+ obj = findDspaceObjectByUrl(context, notification.getObject().getAsObject());
+ // use as:object for sender
+ } else {
+ // this means we're receiving the notification
+ obj = findDspaceObjectByUrl(context, notification.getObject().getAsSubject());
+ // use as:subject for receiver
+ }
+ }
+ ldnMessage.setObject(obj);
+ if (null != notification.getContext()) {
+ ldnMessage.setContext(findDspaceObjectByUrl(context, notification.getContext().getId()));
+ }
+ ldnMessage.setOrigin(findNotifyService(context, notification.getOrigin()));
+ ldnMessage.setInReplyTo(find(context, notification.getInReplyTo()));
+ ObjectMapper mapper = new ObjectMapper();
+ String message = null;
+ try {
+ message = mapper.writeValueAsString(notification);
+ ldnMessage.setMessage(message);
+ } catch (JsonProcessingException e) {
+ log.error("Notification json can't be correctly processed " +
+ "and stored inside the LDN Message Entity" + ldnMessage);
+ log.error(e);
+ }
+ ldnMessage.setType(StringUtils.joinWith(",", notification.getType()));
+ Set notificationType = notification.getType();
+ if (notificationType == null) {
+ log.error("Notification has no notificationType attribute! " + notification);
+ return null;
+ }
+ ArrayList notificationTypeArrayList = new ArrayList(notificationType);
+ // sorting the list
+ Collections.sort(notificationTypeArrayList);
+ ldnMessage.setActivityStreamType(notificationTypeArrayList.get(0));
+ if (notificationTypeArrayList.size() > 1) {
+ ldnMessage.setCoarNotifyType(notificationTypeArrayList.get(1));
+ }
+ ldnMessage.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_QUEUED);
+ 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());
+
+ update(context, ldnMessage);
+ return ldnMessage;
+ }
+
+ @Override
+ public boolean isValidIp(NotifyServiceEntity origin, String sourceIp) {
+
+ String lowerIp = origin.getLowerIp();
+ String upperIp = origin.getUpperIp();
+
+ try {
+ InetAddress ip = InetAddress.getByName(sourceIp);
+ InetAddress lowerBoundAddress = InetAddress.getByName(lowerIp);
+ InetAddress upperBoundAddress = InetAddress.getByName(upperIp);
+
+ long ipLong = ipToLong(ip);
+ long lowerBoundLong = ipToLong(lowerBoundAddress);
+ long upperBoundLong = ipToLong(upperBoundAddress);
+
+ return ipLong >= lowerBoundLong && ipLong <= upperBoundLong;
+ } catch (UnknownHostException e) {
+ return false;
+ }
+ }
+
+ private long ipToLong(InetAddress ip) {
+ byte[] octets = ip.getAddress();
+ long result = 0;
+ for (byte octet : octets) {
+ result <<= 8;
+ result |= octet & 0xff;
+ }
+ return result;
+ }
+
+ @Override
+ public void update(Context context, LDNMessageEntity ldnMessage) throws SQLException {
+ // move the queue_status from UNTRUSTED to QUEUED if origin is a known NotifyService
+ if (ldnMessage.getOrigin() != null &&
+ LDNMessageEntity.QUEUE_STATUS_UNTRUSTED.compareTo(ldnMessage.getQueueStatus()) == 0) {
+ ldnMessage.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_QUEUED);
+ }
+ ldnMessageDao.save(context, ldnMessage);
+ UUID notificationUUID = UUID.fromString(ldnMessage.getID().replace(LDN_ID_PREFIX, ""));
+ ArrayList identifiersList = new ArrayList();
+ identifiersList.add(ldnMessage.getID());
+ context.addEvent(
+ new Event(Event.MODIFY, Constants.LDN_MESSAGE,
+ notificationUUID,
+ IndexableLDNNotification.TYPE, identifiersList));
+ }
+
+ private DSpaceObject findDspaceObjectByUrl(Context context, String url) throws SQLException {
+ String dspaceUrl = configurationService.getProperty("dspace.ui.url") + "/handle/";
+
+ if (StringUtils.startsWith(url, dspaceUrl)) {
+ return handleService.resolveToObject(context, url.substring(dspaceUrl.length()));
+ }
+
+ String handleResolver = configurationService.getProperty("handle.canonical.prefix", "https://hdl.handle.net/");
+ if (StringUtils.startsWith(url, handleResolver)) {
+ return handleService.resolveToObject(context, url.substring(handleResolver.length()));
+ }
+
+ dspaceUrl = configurationService.getProperty("dspace.ui.url") + "/items/";
+ if (StringUtils.startsWith(url, dspaceUrl)) {
+ return itemService.find(context, UUID.fromString(url.substring(dspaceUrl.length())));
+ }
+
+ return null;
+ }
+
+ public NotifyServiceEntity findNotifyService(Context context, Service service) throws SQLException {
+ return notifyServiceDao.findByLdnUrl(context, service.getInbox());
+ }
+
+ @Override
+ public List findOldestMessagesToProcess(Context context) throws SQLException {
+ List result = null;
+ int max_attempts = configurationService.getIntProperty("ldn.processor.max.attempts");
+ result = ldnMessageDao.findOldestMessageToProcess(context, max_attempts);
+ return result;
+ }
+
+ @Override
+ public List findMessagesToBeReprocessed(Context context) throws SQLException {
+ List result = null;
+ result = ldnMessageDao.findMessagesToBeReprocessed(context);
+ return result;
+ }
+
+ @Override
+ public List findProcessingTimedoutMessages(Context context) throws SQLException {
+ List result = null;
+ int max_attempts = configurationService.getIntProperty("ldn.processor.max.attempts");
+ result = ldnMessageDao.findProcessingTimedoutMessages(context, max_attempts);
+ return result;
+ }
+
+ @Override
+ public int extractAndProcessMessageFromQueue(Context context) throws SQLException {
+ int count = 0;
+ int timeoutInMinutes = configurationService.getIntProperty("ldn.processor.queue.msg.timeout", 60);
+
+ List messages = findOldestMessagesToProcess(context);
+ messages.addAll(findMessagesToBeReprocessed(context));
+
+ Optional msgOpt = getSingleMessageEntity(messages);
+
+ while (msgOpt.isPresent()) {
+ LDNProcessor processor = null;
+ LDNMessageEntity msg = msgOpt.get();
+ processor = ldnRouter.route(msg);
+ try {
+ boolean isServiceDisabled = !isServiceEnabled(msg);
+ if (processor == null || isServiceDisabled) {
+ log.warn("No processor found for LDN message " + msg);
+ Integer status = isServiceDisabled ? LDNMessageEntity.QUEUE_STATUS_UNTRUSTED
+ : LDNMessageEntity.QUEUE_STATUS_UNMAPPED_ACTION;
+ msg.setQueueStatus(status);
+ msg.setQueueAttempts(msg.getQueueAttempts() + 1);
+ update(context, msg);
+ } else {
+ msg.setQueueLastStartTime(new Date());
+ msg.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_PROCESSING);
+ msg.setQueueTimeout(DateUtils.addMinutes(new Date(), timeoutInMinutes));
+ update(context, msg);
+ ObjectMapper mapper = new ObjectMapper();
+ Notification notification = mapper.readValue(msg.getMessage(), Notification.class);
+ processor.process(context, notification);
+ msg.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_PROCESSED);
+ count++;
+ }
+ } catch (JsonSyntaxException jse) {
+ log.error("Unable to read JSON notification from LdnMessage " + msg, jse);
+ msg.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_FAILED);
+ } catch (Exception e) {
+ log.error(e);
+ msg.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_FAILED);
+ } finally {
+ msg.setQueueAttempts(msg.getQueueAttempts() + 1);
+ update(context, msg);
+ }
+
+ messages = findOldestMessagesToProcess(context);
+ messages.addAll(findMessagesToBeReprocessed(context));
+ msgOpt = getSingleMessageEntity(messages);
+ }
+ return count;
+ }
+
+ private boolean isServiceEnabled(LDNMessageEntity msg) {
+ String localInboxUrl = configurationService.getProperty("ldn.notify.inbox");
+ if (msg.getTarget() == null || StringUtils.equals(msg.getTarget().getLdnUrl(), localInboxUrl)) {
+ return msg.getOrigin().isEnabled();
+ }
+ return msg.getTarget().isEnabled();
+ }
+
+ @Override
+ public int checkQueueMessageTimeout(Context context) throws SQLException {
+ int count = 0;
+ int maxAttempts = configurationService.getIntProperty("ldn.processor.max.attempts", 5);
+ /*
+ * 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
+ */
+ Optional msgOpt = getSingleMessageEntity(findProcessingTimedoutMessages(context));
+
+ while (msgOpt.isPresent()) {
+ LDNMessageEntity msg = msgOpt.get();
+ try {
+ if (msg.getQueueAttempts() >= maxAttempts) {
+ msg.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_FAILED);
+ } else {
+ msg.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_QUEUED);
+ }
+ update(context, msg);
+ count++;
+ } catch (SQLException e) {
+ log.error("Can't update LDN message " + msg, e);
+ }
+ msgOpt = getSingleMessageEntity(findProcessingTimedoutMessages(context));
+ }
+ return count;
+ }
+
+ public Optional getSingleMessageEntity(Collection messages) {
+ return messages.stream().findFirst();
+ }
+
+ @Override
+ public NotifyRequestStatus findRequestsByItem(Context context, Item item) throws SQLException {
+ NotifyRequestStatus result = new NotifyRequestStatus();
+ result.setItemUuid(item.getID());
+ List msgs = ldnMessageDao.findAllMessagesByItem(
+ context, item, "Offer");
+ if (msgs != null && !msgs.isEmpty()) {
+ for (LDNMessageEntity msg : msgs) {
+ RequestStatus offer = new RequestStatus();
+ NotifyServiceEntity nse = msg.getOrigin();
+ if (nse == null) {
+ nse = msg.getTarget();
+ }
+ offer.setServiceName(nse == null ? "Unknown Service" : nse.getName());
+ offer.setServiceUrl(nse == null ? "" : nse.getUrl());
+ offer.setOfferType(LDNUtils.getNotifyType(msg.getCoarNotifyType()));
+ List acks = ldnMessageDao.findAllRelatedMessagesByItem(
+ context, msg, item, "Accept", "TentativeReject", "TentativeAccept", "Announce");
+ if (acks == null || acks.isEmpty()) {
+ offer.setStatus(NotifyRequestStatusEnum.REQUESTED);
+ } else if (acks.stream()
+ .filter(c -> (c.getActivityStreamType().equalsIgnoreCase("TentativeAccept") ||
+ c.getActivityStreamType().equalsIgnoreCase("Accept")))
+ .findAny().isPresent()) {
+ offer.setStatus(NotifyRequestStatusEnum.ACCEPTED);
+ } else if (acks.stream()
+ .filter(c -> c.getActivityStreamType().equalsIgnoreCase("TentativeReject"))
+ .findAny().isPresent()) {
+ offer.setStatus(NotifyRequestStatusEnum.REJECTED);
+ }
+ if (acks.stream().filter(
+ c -> c.getActivityStreamType().equalsIgnoreCase("Announce"))
+ .findAny().isEmpty()) {
+ result.addRequestStatus(offer);
+ }
+ }
+ }
+ return result;
+ }
+
+ public void delete(Context context, LDNMessageEntity ldnMessage) throws SQLException {
+ ldnMessageDao.delete(context, ldnMessage);
+ }
+
+ @Override
+ public boolean isTargetCurrent(Notification notification) {
+ String localInboxUrl = configurationService.getProperty("ldn.notify.inbox");
+ return StringUtils.equals(notification.getTarget().getInbox(), localInboxUrl);
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/NotifyPatternToTriggerImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/NotifyPatternToTriggerImpl.java
new file mode 100644
index 000000000000..89ec4abe58d9
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/NotifyPatternToTriggerImpl.java
@@ -0,0 +1,62 @@
+/**
+ * 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.service.impl;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import org.dspace.app.ldn.NotifyPatternToTrigger;
+import org.dspace.app.ldn.dao.NotifyPatternToTriggerDao;
+import org.dspace.app.ldn.service.NotifyPatternToTriggerService;
+import org.dspace.content.Item;
+import org.dspace.core.Context;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Implementation of {@link NotifyPatternToTriggerService}.
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+public class NotifyPatternToTriggerImpl implements NotifyPatternToTriggerService {
+
+ @Autowired(required = true)
+ private NotifyPatternToTriggerDao notifyPatternToTriggerDao;
+
+ @Override
+ public List findAll(Context context) throws SQLException {
+ return notifyPatternToTriggerDao.findAll(context, NotifyPatternToTrigger.class);
+ }
+
+ @Override
+ public List findByItem(Context context, Item item) throws SQLException {
+ return notifyPatternToTriggerDao.findByItem(context, item);
+ }
+
+ @Override
+ public List findByItemAndPattern(Context context, Item item, String pattern)
+ throws SQLException {
+ return notifyPatternToTriggerDao.findByItemAndPattern(context, item, pattern);
+ }
+
+ @Override
+ public NotifyPatternToTrigger create(Context context) throws SQLException {
+ NotifyPatternToTrigger notifyPatternToTrigger = new NotifyPatternToTrigger();
+ return notifyPatternToTriggerDao.create(context, notifyPatternToTrigger);
+ }
+
+ @Override
+ public void update(Context context, NotifyPatternToTrigger notifyPatternToTrigger) throws SQLException {
+ notifyPatternToTriggerDao.save(context, notifyPatternToTrigger);
+ }
+
+ @Override
+ public void delete(Context context, NotifyPatternToTrigger notifyPatternToTrigger) throws SQLException {
+ notifyPatternToTriggerDao.delete(context, notifyPatternToTrigger);
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/NotifyServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/NotifyServiceImpl.java
new file mode 100644
index 000000000000..87be008371aa
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/NotifyServiceImpl.java
@@ -0,0 +1,67 @@
+/**
+ * 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.service.impl;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import org.dspace.app.ldn.NotifyServiceEntity;
+import org.dspace.app.ldn.dao.NotifyServiceDao;
+import org.dspace.app.ldn.service.NotifyService;
+import org.dspace.core.Context;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Implementation of {@link NotifyService}.
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+public class NotifyServiceImpl implements NotifyService {
+
+ @Autowired(required = true)
+ private NotifyServiceDao notifyServiceDao;
+
+ @Override
+ public List findAll(Context context) throws SQLException {
+ return notifyServiceDao.findAll(context, NotifyServiceEntity.class);
+ }
+
+ @Override
+ public NotifyServiceEntity find(Context context, Integer id) throws SQLException {
+ return notifyServiceDao.findByID(context, NotifyServiceEntity.class, id);
+ }
+
+ @Override
+ public NotifyServiceEntity create(Context context, String name) throws SQLException {
+ NotifyServiceEntity notifyServiceEntity = new NotifyServiceEntity();
+ notifyServiceEntity.setName(name);
+ return notifyServiceDao.create(context, notifyServiceEntity);
+ }
+
+ @Override
+ public void update(Context context, NotifyServiceEntity notifyServiceEntity) throws SQLException {
+ notifyServiceDao.save(context, notifyServiceEntity);
+ }
+
+ @Override
+ public void delete(Context context, NotifyServiceEntity notifyServiceEntity) throws SQLException {
+ notifyServiceDao.delete(context, notifyServiceEntity);
+ }
+
+ @Override
+ public NotifyServiceEntity findByLdnUrl(Context context, String ldnUrl) throws SQLException {
+ return notifyServiceDao.findByLdnUrl(context, ldnUrl);
+ }
+
+ @Override
+ public List findManualServicesByInboundPattern(Context context, String pattern)
+ throws SQLException {
+ return notifyServiceDao.findManualServicesByInboundPattern(context, pattern);
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/NotifyServiceInboundPatternServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/NotifyServiceInboundPatternServiceImpl.java
new file mode 100644
index 000000000000..c699d9fd0376
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/NotifyServiceInboundPatternServiceImpl.java
@@ -0,0 +1,59 @@
+/**
+ * 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.service.impl;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import org.dspace.app.ldn.NotifyServiceEntity;
+import org.dspace.app.ldn.NotifyServiceInboundPattern;
+import org.dspace.app.ldn.dao.NotifyServiceInboundPatternDao;
+import org.dspace.app.ldn.service.NotifyServiceInboundPatternService;
+import org.dspace.core.Context;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Implementation Service class for the {@link NotifyServiceInboundPatternService}.
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.com)
+ */
+public class NotifyServiceInboundPatternServiceImpl implements NotifyServiceInboundPatternService {
+
+ @Autowired
+ private NotifyServiceInboundPatternDao inboundPatternDao;
+
+ @Override
+ public NotifyServiceInboundPattern findByServiceAndPattern(Context context,
+ NotifyServiceEntity notifyServiceEntity,
+ String pattern) throws SQLException {
+ return inboundPatternDao.findByServiceAndPattern(context, notifyServiceEntity, pattern);
+ }
+
+ @Override
+ public List findAutomaticPatterns(Context context) throws SQLException {
+ return inboundPatternDao.findAutomaticPatterns(context);
+ }
+
+ @Override
+ public NotifyServiceInboundPattern create(Context context, NotifyServiceEntity notifyServiceEntity)
+ throws SQLException {
+ NotifyServiceInboundPattern inboundPattern = new NotifyServiceInboundPattern();
+ inboundPattern.setNotifyService(notifyServiceEntity);
+ return inboundPatternDao.create(context, inboundPattern);
+ }
+
+ @Override
+ public void update(Context context, NotifyServiceInboundPattern inboundPattern) throws SQLException {
+ inboundPatternDao.save(context, inboundPattern);
+ }
+
+ @Override
+ public void delete(Context context, NotifyServiceInboundPattern inboundPattern) throws SQLException {
+ inboundPatternDao.delete(context, inboundPattern);
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/utility/LDNUtils.java b/dspace-api/src/main/java/org/dspace/app/ldn/utility/LDNUtils.java
new file mode 100644
index 000000000000..949da655bc70
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/ldn/utility/LDNUtils.java
@@ -0,0 +1,96 @@
+/**
+ * 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.utility;
+
+import static org.apache.commons.lang3.StringUtils.EMPTY;
+
+import java.util.UUID;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Some linked data notification utilities.
+ */
+public class LDNUtils {
+
+ private final static Pattern UUID_REGEX_PATTERN = Pattern.compile(
+ "\\p{XDigit}{8}-\\p{XDigit}{4}-\\p{XDigit}{4}-\\p{XDigit}{4}-\\p{XDigit}{12}");
+
+ private final static String SIMPLE_PROTOCOL_REGEX = "^(http[s]?://www\\.|http[s]?://|www\\.)";
+
+ /**
+ *
+ */
+ private LDNUtils() {
+
+ }
+
+ /**
+ * Whether the URL contains an UUID. Used to determine item id from item URL.
+ *
+ * @param url item URL
+ * @return boolean true if URL has UUID, false otherwise
+ */
+ public static boolean hasUUIDInURL(String url) {
+ Matcher matcher = UUID_REGEX_PATTERN.matcher(url);
+
+ return matcher.find();
+ }
+
+ /**
+ * Extract UUID from URL.
+ *
+ * @param url item URL
+ * @return UUID item id
+ */
+ public static UUID getUUIDFromURL(String url) {
+ Matcher matcher = UUID_REGEX_PATTERN.matcher(url);
+ StringBuilder handle = new StringBuilder();
+ if (matcher.find()) {
+ handle.append(matcher.group(0));
+ }
+ return UUID.fromString(handle.toString());
+ }
+
+ /**
+ * Remove http or https protocol from URL.
+ *
+ * @param url URL
+ * @return String URL without protocol
+ */
+ public static String removedProtocol(String url) {
+ return url.replaceFirst(SIMPLE_PROTOCOL_REGEX, EMPTY);
+ }
+
+ /**
+ * Custom context resolver processing. Currently converting DOI URL to DOI id.
+ *
+ * @param value context ietf:cite-as
+ * @return String ietf:cite-as identifier
+ */
+ public static String processContextResolverId(String value) {
+ String resolverId = value;
+ resolverId = resolverId.replace("https://doi.org/", "doi:");
+
+ return resolverId;
+ }
+
+ /**
+ * Clear the coarNotifyType from the source code.
+ *
+ * @param coarNotifyType coar Notify Type to sanitize
+ * @return String just the notify type
+ */
+ public static String getNotifyType(String coarNotifyType) {
+ String justNotifyType = coarNotifyType;
+ justNotifyType = justNotifyType.substring(justNotifyType.lastIndexOf(":") + 1);
+ justNotifyType = justNotifyType.replace("Action", "");
+ return justNotifyType;
+ }
+
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItem.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItem.java
index cdefd1298c6e..bf2bfb4d60b2 100644
--- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItem.java
+++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItem.java
@@ -8,19 +8,19 @@
package org.dspace.app.requestitem;
import java.util.Date;
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.FetchType;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-import javax.persistence.JoinColumn;
-import javax.persistence.ManyToOne;
-import javax.persistence.SequenceGenerator;
-import javax.persistence.Table;
-import javax.persistence.Temporal;
-import javax.persistence.TemporalType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.SequenceGenerator;
+import jakarta.persistence.Table;
+import jakarta.persistence.Temporal;
+import jakarta.persistence.TemporalType;
import org.dspace.content.Bitstream;
import org.dspace.content.Item;
import org.dspace.core.Context;
diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemEmailNotifier.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemEmailNotifier.java
index 6499c45a7830..c489fb4b3ff0 100644
--- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemEmailNotifier.java
+++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemEmailNotifier.java
@@ -11,11 +11,11 @@
import java.io.IOException;
import java.sql.SQLException;
import java.util.List;
-import javax.annotation.ManagedBean;
-import javax.inject.Inject;
-import javax.inject.Singleton;
-import javax.mail.MessagingException;
+import jakarta.annotation.ManagedBean;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+import jakarta.mail.MessagingException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.app.requestitem.service.RequestItemService;
diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/dao/impl/RequestItemDAOImpl.java b/dspace-api/src/main/java/org/dspace/app/requestitem/dao/impl/RequestItemDAOImpl.java
index 008174ded88c..c76bd50d1910 100644
--- a/dspace-api/src/main/java/org/dspace/app/requestitem/dao/impl/RequestItemDAOImpl.java
+++ b/dspace-api/src/main/java/org/dspace/app/requestitem/dao/impl/RequestItemDAOImpl.java
@@ -9,11 +9,11 @@
import java.sql.SQLException;
import java.util.Iterator;
-import javax.persistence.Query;
-import javax.persistence.criteria.CriteriaBuilder;
-import javax.persistence.criteria.CriteriaQuery;
-import javax.persistence.criteria.Root;
+import jakarta.persistence.Query;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Root;
import org.dspace.app.requestitem.RequestItem;
import org.dspace.app.requestitem.RequestItem_;
import org.dspace.app.requestitem.dao.RequestItemDAO;
@@ -44,8 +44,12 @@ public RequestItem findByToken(Context context, String token) throws SQLExceptio
}
@Override
public Iterator findByItem(Context context, Item item) throws SQLException {
- Query query = createQuery(context, "FROM RequestItem WHERE item_id= :uuid");
- query.setParameter("uuid", item.getID());
+ CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
+ CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, RequestItem.class);
+ Root requestItemRoot = criteriaQuery.from(RequestItem.class);
+ criteriaQuery.select(requestItemRoot);
+ criteriaQuery.where(criteriaBuilder.equal(requestItemRoot.get(RequestItem_.item), item));
+ Query query = createQuery(context, criteriaQuery);
return iterate(query);
}
}
diff --git a/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAService.java b/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAService.java
index ead725e842c4..9ee5ca55cc6d 100644
--- a/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAService.java
+++ b/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAService.java
@@ -12,8 +12,8 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
-import javax.annotation.PostConstruct;
+import jakarta.annotation.PostConstruct;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
@@ -93,7 +93,7 @@ private void init() {
* @param query ISSN string to pass in an "issn equals" API query
* @return SHERPAResponse containing an error or journal policies
*/
- @Cacheable(key = "#query", cacheNames = "sherpa.searchByJournalISSN")
+ @Cacheable(key = "#query", condition = "#query != null", cacheNames = "sherpa.searchByJournalISSN")
public SHERPAResponse searchByJournalISSN(String query) {
return performRequest("publication", "issn", "equals", query, 0, 1);
}
diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionProvider.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionProvider.java
new file mode 100644
index 000000000000..f5acd2ccbc0f
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionProvider.java
@@ -0,0 +1,140 @@
+/**
+ * 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.suggestion;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.UUID;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.dspace.content.service.ItemService;
+import org.dspace.core.Context;
+import org.dspace.external.model.ExternalDataObject;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Suggestion provider that read the suggestion from the local suggestion solr
+ * core
+ *
+ * @author Andrea Bollini (andrea.bollini at 4science dot it)
+ *
+ */
+public abstract class SolrSuggestionProvider implements SuggestionProvider {
+ private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(SolrSuggestionProvider.class);
+
+ @Autowired
+ protected ItemService itemService;
+
+ @Autowired
+ protected SolrSuggestionStorageService solrSuggestionStorageService;
+
+ private String sourceName;
+
+ public String getSourceName() {
+ return sourceName;
+ }
+
+ public void setSourceName(String sourceName) {
+ this.sourceName = sourceName;
+ }
+
+ public void setItemService(ItemService itemService) {
+ this.itemService = itemService;
+ }
+
+ @Override
+ public long countAllTargets(Context context) {
+ try {
+ return this.solrSuggestionStorageService.countAllTargets(context, sourceName);
+ } catch (SolrServerException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public long countUnprocessedSuggestionByTarget(Context context, UUID target) {
+ try {
+ return this.solrSuggestionStorageService.countUnprocessedSuggestionByTarget(context, sourceName, target);
+ } catch (SolrServerException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public List findAllUnprocessedSuggestions(Context context, UUID target, int pageSize, long offset,
+ boolean ascending) {
+
+ try {
+ return this.solrSuggestionStorageService.findAllUnprocessedSuggestions(context, sourceName,
+ target, pageSize, offset, ascending);
+ } catch (SolrServerException | IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ @Override
+ public List findAllTargets(Context context, int pageSize, long offset) {
+ try {
+ return this.solrSuggestionStorageService.findAllTargets(context, sourceName, pageSize, offset);
+ } catch (SolrServerException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public Suggestion findUnprocessedSuggestion(Context context, UUID target, String id) {
+ try {
+ return this.solrSuggestionStorageService.findUnprocessedSuggestion(context, sourceName, target, id);
+ } catch (SolrServerException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public SuggestionTarget findTarget(Context context, UUID target) {
+ try {
+ return this.solrSuggestionStorageService.findTarget(context, sourceName, target);
+ } catch (SolrServerException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void rejectSuggestion(Context context, UUID target, String idPart) {
+ Suggestion suggestion = findUnprocessedSuggestion(context, target, idPart);
+ try {
+ solrSuggestionStorageService.flagSuggestionAsProcessed(suggestion);
+ } catch (SolrServerException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void flagRelatedSuggestionsAsProcessed(Context context, ExternalDataObject externalDataObject) {
+ if (!isExternalDataObjectPotentiallySuggested(context, externalDataObject)) {
+ return;
+ }
+ try {
+ solrSuggestionStorageService.flagAllSuggestionAsProcessed(sourceName, externalDataObject.getId());
+ } catch (SolrServerException | IOException e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * check if the externalDataObject may have suggestion
+ * @param context
+ * @param externalDataObject
+ * @return true if the externalDataObject could be suggested by this provider
+ * (i.e. it comes from a DataProvider used by this suggestor)
+ */
+ protected abstract boolean isExternalDataObjectPotentiallySuggested(Context context,
+ ExternalDataObject externalDataObject);
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageService.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageService.java
new file mode 100644
index 000000000000..b7de6146f27e
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageService.java
@@ -0,0 +1,191 @@
+/**
+ * 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.suggestion;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.UUID;
+
+import org.apache.solr.client.solrj.SolrServerException;
+import org.dspace.core.Context;
+
+/**
+ * Service to deal with the local suggestion solr core used by the
+ * SolrSuggestionProvider(s)
+ *
+ * @author Andrea Bollini (andrea.bollini at 4science dot it)
+ * @author Luca Giamminonni (luca.giamminonni at 4science dot it)
+ *
+ */
+public interface SolrSuggestionStorageService {
+ public static final String SOURCE = "source";
+ /** This is the URI Part of the suggestion source:target:id */
+ public static final String SUGGESTION_FULLID = "suggestion_fullid";
+ public static final String SUGGESTION_ID = "suggestion_id";
+ public static final String TARGET_ID = "target_id";
+ public static final String TITLE = "title";
+ public static final String DATE = "date";
+ public static final String DISPLAY = "display";
+ public static final String CONTRIBUTORS = "contributors";
+ public static final String ABSTRACT = "abstract";
+ public static final String CATEGORY = "category";
+ public static final String EXTERNAL_URI = "external-uri";
+ public static final String PROCESSED = "processed";
+ public static final String SCORE = "trust";
+ public static final String EVIDENCES = "evidences";
+
+ /**
+ * Add a new suggestion to SOLR
+ *
+ * @param suggestion
+ * @param force true if the suggestion must be reindexed
+ * @param commit
+ * @throws IOException
+ * @throws SolrServerException
+ */
+ public void addSuggestion(Suggestion suggestion, boolean force, boolean commit)
+ throws SolrServerException, IOException;
+
+ /**
+ * Return true if the suggestion is already in SOLR and flagged as processed
+ *
+ * @param suggestion
+ * @return true if the suggestion is already in SOLR and flagged as processed
+ * @throws IOException
+ * @throws SolrServerException
+ */
+ public boolean exist(Suggestion suggestion) throws SolrServerException, IOException;
+
+ /**
+ * Delete a suggestion from SOLR if any
+ *
+ * @param suggestion
+ * @throws IOException
+ * @throws SolrServerException
+ */
+ public void deleteSuggestion(Suggestion suggestion) throws SolrServerException, IOException;
+
+ /**
+ * Flag a suggestion as processed in SOLR if any
+ *
+ * @param suggestion
+ * @throws IOException
+ * @throws SolrServerException
+ */
+ public void flagSuggestionAsProcessed(Suggestion suggestion) throws SolrServerException, IOException;
+
+ /**
+ * Delete all the suggestions from SOLR if any related to a specific target
+ *
+ * @param target
+ * @throws IOException
+ * @throws SolrServerException
+ */
+ public void deleteTarget(SuggestionTarget target) throws SolrServerException, IOException;
+
+ /**
+ * Performs an explicit commit, causing pending documents to be committed for
+ * indexing.
+ *
+ * @throws SolrServerException
+ * @throws IOException
+ */
+ void commit() throws SolrServerException, IOException;
+
+ /**
+ * Flag all the suggestion related to the given source and id as processed.
+ *
+ * @param source the source name
+ * @param idPart the id's last part
+ * @throws SolrServerException
+ * @throws IOException
+ */
+ void flagAllSuggestionAsProcessed(String source, String idPart) throws SolrServerException, IOException;
+
+ /**
+ * Count all the targets related to the given source.
+ *
+ * @param source the source name
+ * @return the target's count
+ * @throws IOException
+ * @throws SolrServerException
+ */
+ long countAllTargets(Context context, String source) throws SolrServerException, IOException;
+
+ /**
+ * Count all the unprocessed suggestions related to the given source and target.
+ *
+ * @param context the DSpace Context
+ * @param source the source name
+ * @param target the target id
+ * @return the suggestion count
+ * @throws SolrServerException
+ * @throws IOException
+ */
+ long countUnprocessedSuggestionByTarget(Context context, String source, UUID target)
+ throws SolrServerException, IOException;
+
+ /**
+ * Find all the unprocessed suggestions related to the given source and target.
+ *
+ * @param context the DSpace Context
+ * @param source the source name
+ * @param target the target id
+ * @param pageSize the page size
+ * @param offset the page offset
+ * @param ascending true to retrieve the suggestions ordered by score
+ * ascending
+ * @return the found suggestions
+ * @throws SolrServerException
+ * @throws IOException
+ */
+ List findAllUnprocessedSuggestions(Context context, String source, UUID target,
+ int pageSize, long offset, boolean ascending) throws SolrServerException, IOException;
+
+ /**
+ *
+ * Find all the suggestion targets related to the given source.
+ *
+ * @param context the DSpace Context
+ * @param source the source name
+ * @param pageSize the page size
+ * @param offset the page offset
+ * @return the found suggestion targets
+ * @throws SolrServerException
+ * @throws IOException
+ */
+ List findAllTargets(Context context, String source, int pageSize, long offset)
+ throws SolrServerException, IOException;
+
+ /**
+ * Find an unprocessed suggestion by the given source, target id and suggestion
+ * id.
+ *
+ * @param context the DSpace Context
+ * @param source the source name
+ * @param target the target id
+ * @param id the suggestion id
+ * @return the suggestion, if any
+ * @throws SolrServerException
+ * @throws IOException
+ */
+ Suggestion findUnprocessedSuggestion(Context context, String source, UUID target, String id)
+ throws SolrServerException, IOException;
+
+ /**
+ * Find a suggestion target by the given source and target.
+ *
+ * @param context the DSpace Context
+ * @param source the source name
+ * @param target the target id
+ * @return the suggestion target, if any
+ * @throws SolrServerException
+ * @throws IOException
+ */
+ SuggestionTarget findTarget(Context context, String source, UUID target) throws SolrServerException, IOException;
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageServiceImpl.java
new file mode 100644
index 000000000000..3c2ad71846a6
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageServiceImpl.java
@@ -0,0 +1,361 @@
+/**
+ * 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.suggestion;
+
+import static org.apache.commons.collections.CollectionUtils.isEmpty;
+
+import java.io.IOException;
+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.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;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.HttpSolrClient;
+import org.apache.solr.client.solrj.response.FacetField;
+import org.apache.solr.client.solrj.response.FacetField.Count;
+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;
+import org.dspace.core.Context;
+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)
+ *
+ * @author Andrea Bollini (andrea.bollini at 4science dot it)
+ *
+ */
+public class SolrSuggestionStorageServiceImpl implements SolrSuggestionStorageService {
+
+ private static final Logger log = LogManager.getLogger(SolrSuggestionStorageServiceImpl.class);
+
+ protected SolrClient solrSuggestionClient;
+
+ @Autowired
+ private ItemService itemService;
+
+ /**
+ * Get solr client which use suggestion core
+ *
+ * @return solr client
+ */
+ protected SolrClient getSolr() {
+ if (solrSuggestionClient == null) {
+ String solrService = DSpaceServicesFactory.getInstance().getConfigurationService()
+ .getProperty("suggestion.solr.server", "http://localhost:8983/solr/suggestion");
+ solrSuggestionClient = new HttpSolrClient.Builder(solrService).build();
+ }
+ return solrSuggestionClient;
+ }
+
+ @Override
+ public void addSuggestion(Suggestion suggestion, boolean force, boolean commit)
+ throws SolrServerException, IOException {
+ if (force || !exist(suggestion)) {
+ 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]);
+ document.addField(TARGET_ID, suggestion.getTarget().getID().toString());
+ document.addField(DISPLAY, suggestion.getDisplay());
+ document.addField(TITLE, getFirstValue(suggestion, "dc", "title", null));
+ document.addField(DATE, getFirstValue(suggestion, "dc", "date", "issued"));
+ document.addField(CONTRIBUTORS, getAllValues(suggestion, "dc", "contributor", "author"));
+ document.addField(ABSTRACT, getFirstValue(suggestion, "dc", "description", "abstract"));
+ document.addField(CATEGORY, getAllValues(suggestion, "dc", "source", null));
+ document.addField(EXTERNAL_URI, suggestion.getExternalSourceUri());
+ document.addField(SCORE, suggestion.getScore());
+ document.addField(PROCESSED, false);
+ document.addField(EVIDENCES, jsonMapper.writeValueAsString(suggestion.getEvidences()));
+ getSolr().add(document);
+ if (commit) {
+ getSolr().commit();
+ }
+ }
+ }
+
+ @Override
+ public void commit() throws SolrServerException, IOException {
+ getSolr().commit();
+ }
+
+ private List getAllValues(Suggestion suggestion, String schema, String element, String qualifier) {
+ return suggestion.getMetadata().stream()
+ .filter(st -> StringUtils.isNotBlank(st.getValue()) && StringUtils.equals(st.getSchema(), schema)
+ && StringUtils.equals(st.getElement(), element)
+ && StringUtils.equals(st.getQualifier(), qualifier))
+ .map(st -> st.getValue()).collect(Collectors.toList());
+ }
+
+ private String getFirstValue(Suggestion suggestion, String schema, String element, String qualifier) {
+ return suggestion.getMetadata().stream()
+ .filter(st -> StringUtils.isNotBlank(st.getValue())
+ && StringUtils.equals(st.getSchema(), schema)
+ && StringUtils.equals(st.getElement(), element)
+ && StringUtils.equals(st.getQualifier(), qualifier))
+ .map(st -> st.getValue()).findFirst().orElse(null);
+ }
+
+ @Override
+ public boolean exist(Suggestion suggestion) throws SolrServerException, IOException {
+ SolrQuery query = new SolrQuery(
+ SUGGESTION_FULLID + ":\"" + suggestion.getID() + "\" AND " + PROCESSED + ":true");
+ return getSolr().query(query).getResults().getNumFound() == 1;
+ }
+
+ @Override
+ public void deleteSuggestion(Suggestion suggestion) throws SolrServerException, IOException {
+ getSolr().deleteById(suggestion.getID());
+ getSolr().commit();
+ }
+
+ @Override
+ public void flagSuggestionAsProcessed(Suggestion suggestion) throws SolrServerException, IOException {
+ SolrInputDocument sdoc = new SolrInputDocument();
+ sdoc.addField(SUGGESTION_FULLID, suggestion.getID());
+ Map fieldModifier = new HashMap<>(1);
+ fieldModifier.put("set", true);
+ sdoc.addField(PROCESSED, fieldModifier); // add the map as the field value
+ getSolr().add(sdoc);
+ getSolr().commit();
+ }
+
+ @Override
+ public void flagAllSuggestionAsProcessed(String source, String idPart) throws SolrServerException, IOException {
+ SolrQuery query = new SolrQuery(SOURCE + ":" + source + " AND " + SUGGESTION_ID + ":\"" + idPart + "\"");
+ query.setRows(Integer.MAX_VALUE);
+ query.setFields(SUGGESTION_FULLID);
+ SolrDocumentList results = getSolr().query(query).getResults();
+ if (results.getNumFound() > 0) {
+ for (SolrDocument rDoc : results) {
+ SolrInputDocument sdoc = new SolrInputDocument();
+ sdoc.addField(SUGGESTION_FULLID, rDoc.getFieldValue(SUGGESTION_FULLID));
+ Map fieldModifier = new HashMap<>(1);
+ fieldModifier.put("set", true);
+ sdoc.addField(PROCESSED, fieldModifier); // add the map as the field value
+ getSolr().add(sdoc);
+ }
+ }
+ getSolr().commit();
+ }
+
+ @Override
+ public void deleteTarget(SuggestionTarget target) throws SolrServerException, IOException {
+ getSolr().deleteByQuery(
+ SOURCE + ":" + target.getSource() + " AND " + TARGET_ID + ":" + target.getTarget().getID().toString());
+ getSolr().commit();
+ }
+
+ @Override
+ public long countAllTargets(Context context, String source) throws SolrServerException, IOException {
+ SolrQuery solrQuery = new SolrQuery();
+ solrQuery.setRows(0);
+ solrQuery.setQuery(SOURCE + ":" + source);
+ solrQuery.addFilterQuery(PROCESSED + ":false");
+ solrQuery.setFacet(true);
+ solrQuery.setFacetMinCount(1);
+ solrQuery.addFacetField(TARGET_ID);
+ solrQuery.setFacetLimit(Integer.MAX_VALUE);
+ QueryResponse response = getSolr().query(solrQuery);
+ return response.getFacetField(TARGET_ID).getValueCount();
+ }
+
+ @Override
+ public long countUnprocessedSuggestionByTarget(Context context, String source, UUID target)
+ throws SolrServerException, IOException {
+ SolrQuery solrQuery = new SolrQuery();
+ solrQuery.setRows(0);
+ solrQuery.setQuery("*:*");
+ solrQuery.addFilterQuery(
+ SOURCE + ":" + source,
+ TARGET_ID + ":" + target.toString(),
+ PROCESSED + ":false");
+
+ QueryResponse response = getSolr().query(solrQuery);
+ return response.getResults().getNumFound();
+ }
+
+ @Override
+ public List findAllUnprocessedSuggestions(Context context, String source, UUID target,
+ int pageSize, long offset, boolean ascending) throws SolrServerException, IOException {
+
+ SolrQuery solrQuery = new SolrQuery();
+ solrQuery.setRows(pageSize);
+ solrQuery.setStart((int) offset);
+ solrQuery.setQuery("*:*");
+ solrQuery.addFilterQuery(
+ SOURCE + ":" + source,
+ TARGET_ID + ":" + target.toString(),
+ PROCESSED + ":false");
+
+ if (ascending) {
+ solrQuery.addSort(SortClause.asc("trust"));
+ } else {
+ solrQuery.addSort(SortClause.desc("trust"));
+ }
+
+ solrQuery.addSort(SortClause.desc("date"));
+ solrQuery.addSort(SortClause.asc("title"));
+
+ QueryResponse response = getSolr().query(solrQuery);
+ List suggestions = new ArrayList();
+ for (SolrDocument solrDoc : response.getResults()) {
+ suggestions.add(convertSolrDoc(context, solrDoc, source));
+ }
+ return suggestions;
+
+ }
+
+ @Override
+ public List findAllTargets(Context context, String source, int pageSize, long offset)
+ throws SolrServerException, IOException {
+
+ SolrQuery solrQuery = new SolrQuery();
+ solrQuery.setRows(0);
+ solrQuery.setQuery(SOURCE + ":" + source);
+ solrQuery.addFilterQuery(PROCESSED + ":false");
+ solrQuery.setFacet(true);
+ solrQuery.setFacetMinCount(1);
+ solrQuery.addFacetField(TARGET_ID);
+ solrQuery.setParam(FacetParams.FACET_OFFSET, String.valueOf(offset));
+ solrQuery.setFacetLimit((int) (pageSize));
+ QueryResponse response = getSolr().query(solrQuery);
+ FacetField facetField = response.getFacetField(TARGET_ID);
+ List suggestionTargets = new ArrayList();
+ int idx = 0;
+ for (Count c : facetField.getValues()) {
+ SuggestionTarget target = new SuggestionTarget();
+ target.setSource(source);
+ target.setTotal((int) c.getCount());
+ target.setTarget(findItem(context, c.getName()));
+ suggestionTargets.add(target);
+ idx++;
+ }
+ return suggestionTargets;
+
+ }
+
+ @Override
+ public Suggestion findUnprocessedSuggestion(Context context, String source, UUID target, String id)
+ throws SolrServerException, IOException {
+
+ SolrQuery solrQuery = new SolrQuery();
+ solrQuery.setRows(1);
+ solrQuery.setQuery("*:*");
+ solrQuery.addFilterQuery(
+ SOURCE + ":" + source,
+ TARGET_ID + ":" + target.toString(),
+ SUGGESTION_ID + ":\"" + id + "\"",
+ PROCESSED + ":false");
+
+ SolrDocumentList results = getSolr().query(solrQuery).getResults();
+ return isEmpty(results) ? null : convertSolrDoc(context, results.get(0), source);
+ }
+
+ @Override
+ public SuggestionTarget findTarget(Context context, String source, UUID target)
+ throws SolrServerException, IOException {
+ SolrQuery solrQuery = new SolrQuery();
+ solrQuery.setRows(0);
+ solrQuery.setQuery(SOURCE + ":" + source);
+ solrQuery.addFilterQuery(
+ TARGET_ID + ":" + target.toString(),
+ PROCESSED + ":false");
+ QueryResponse response = getSolr().query(solrQuery);
+ SuggestionTarget sTarget = new SuggestionTarget();
+ sTarget.setSource(source);
+ sTarget.setTotal((int) response.getResults().getNumFound());
+ Item itemTarget = findItem(context, target);
+ if (itemTarget != null) {
+ sTarget.setTarget(itemTarget);
+ } else {
+ return null;
+ }
+ return sTarget;
+ }
+
+ private Suggestion convertSolrDoc(Context context, SolrDocument solrDoc, String sourceName) {
+ Item target = findItem(context, (String) solrDoc.getFieldValue(TARGET_ID));
+
+ Suggestion suggestion = new Suggestion(sourceName, target, (String) solrDoc.getFieldValue(SUGGESTION_ID));
+ suggestion.setDisplay((String) solrDoc.getFieldValue(DISPLAY));
+ suggestion.getMetadata()
+ .add(new MetadataValueDTO("dc", "title", null, null, (String) solrDoc.getFieldValue(TITLE)));
+ suggestion.getMetadata()
+ .add(new MetadataValueDTO("dc", "date", "issued", null, (String) solrDoc.getFieldValue(DATE)));
+ suggestion.getMetadata().add(
+ new MetadataValueDTO("dc", "description", "abstract", null, (String) solrDoc.getFieldValue(ABSTRACT)));
+
+ suggestion.setExternalSourceUri((String) solrDoc.getFieldValue(EXTERNAL_URI));
+ if (solrDoc.containsKey(CATEGORY)) {
+ for (Object o : solrDoc.getFieldValues(CATEGORY)) {
+ suggestion.getMetadata().add(
+ new MetadataValueDTO("dc", "source", null, null, (String) o));
+ }
+ }
+ if (solrDoc.containsKey(CONTRIBUTORS)) {
+ for (Object o : solrDoc.getFieldValues(CONTRIBUTORS)) {
+ suggestion.getMetadata().add(
+ new MetadataValueDTO("dc", "contributor", "author", null, (String) o));
+ }
+ }
+ String evidencesJson = (String) solrDoc.getFieldValue(EVIDENCES);
+ ObjectMapper jsonMapper = new JsonMapper();
+ jsonMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ List evidences = new LinkedList();
+ try {
+ evidences = jsonMapper.readValue(evidencesJson, new TypeReference>() {});
+ } catch (JsonProcessingException e) {
+ log.error(e);
+ }
+ suggestion.getEvidences().addAll(evidences);
+ return suggestion;
+ }
+
+ private Item findItem(Context context, UUID itemId) {
+ try {
+ return itemService.find(context, itemId);
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private Item findItem(Context context, String itemId) {
+ return findItem(context, UUIDUtils.fromString(itemId));
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/Suggestion.java b/dspace-api/src/main/java/org/dspace/app/suggestion/Suggestion.java
new file mode 100644
index 000000000000..7812cbd522e0
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/suggestion/Suggestion.java
@@ -0,0 +1,99 @@
+/**
+ * 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.suggestion;
+
+import java.util.LinkedList;
+import java.util.List;
+
+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 evidences = new LinkedList();
+
+ private List metadata = new LinkedList();
+
+ /** 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;
+ this.id = source + ":" + target.getID().toString() + ":" + idPart;
+ }
+
+ public String getDisplay() {
+ return display;
+ }
+
+ public void setDisplay(String display) {
+ this.display = display;
+ }
+
+ public String getSource() {
+ return source;
+ }
+
+ public String getExternalSourceUri() {
+ return externalSourceUri;
+ }
+
+ public void setExternalSourceUri(String externalSourceUri) {
+ this.externalSourceUri = externalSourceUri;
+ }
+
+ public List getEvidences() {
+ return evidences;
+ }
+
+ public List getMetadata() {
+ return metadata;
+ }
+
+ public Item getTarget() {
+ return target;
+ }
+
+ public String getID() {
+ return id;
+ }
+
+ public Double getScore() {
+ if (evidences != null && evidences.size() > 0) {
+ double score = 0;
+ for (SuggestionEvidence evidence : evidences) {
+ score += evidence.getScore();
+ }
+ return score;
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionEvidence.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionEvidence.java
new file mode 100644
index 000000000000..d7f04929a19a
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionEvidence.java
@@ -0,0 +1,61 @@
+/**
+ * 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.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() {
+ }
+
+ public SuggestionEvidence(String name, double score, String notes) {
+ this.name = name;
+ this.score = score;
+ this.notes = notes;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public double getScore() {
+ return score;
+ }
+
+ public void setScore(double score) {
+ this.score = score;
+ }
+
+ public String getNotes() {
+ return notes;
+ }
+
+ public void setNotes(String notes) {
+ this.notes = notes;
+ }
+
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionProvider.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionProvider.java
new file mode 100644
index 000000000000..7cfc3cfb53b3
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionProvider.java
@@ -0,0 +1,54 @@
+/**
+ * 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.suggestion;
+
+import java.util.List;
+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 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 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);
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionService.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionService.java
new file mode 100644
index 000000000000..41d33026ed0f
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionService.java
@@ -0,0 +1,61 @@
+/**
+ * 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.suggestion;
+
+import java.util.List;
+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 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 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 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 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 getSuggestionProviders();
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionServiceImpl.java
new file mode 100644
index 000000000000..57fe42806b12
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionServiceImpl.java
@@ -0,0 +1,194 @@
+/**
+ * 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.suggestion;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import jakarta.annotation.Resource;
+import org.apache.logging.log4j.Logger;
+import org.dspace.core.Context;
+import org.springframework.stereotype.Service;
+
+@Service
+public class SuggestionServiceImpl implements SuggestionService {
+ private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(SuggestionServiceImpl.class);
+
+ @Resource(name = "suggestionProviders")
+ private Map providersMap;
+
+ @Override
+ public List getSuggestionProviders() {
+ if (providersMap != null) {
+ return providersMap.values().stream().collect(Collectors.toList());
+ }
+ return null;
+ }
+
+ @Override
+ public SuggestionTarget find(Context context, String source, UUID id) {
+ if (providersMap.containsKey(source)) {
+ return providersMap.get(source).findTarget(context, id);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public long countAll(Context context, String source) {
+ if (providersMap.containsKey(source)) {
+ return providersMap.get(source).countAllTargets(context);
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public List findAllTargets(Context context, String source, int pageSize, long offset) {
+ if (providersMap.containsKey(source)) {
+ return providersMap.get(source).findAllTargets(context, pageSize, offset);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public long countAllByTarget(Context context, UUID target) {
+ int count = 0;
+ for (String provider : providersMap.keySet()) {
+ if (providersMap.get(provider).countUnprocessedSuggestionByTarget(context, target) > 0) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ @Override
+ public List findByTarget(Context context, UUID target, int pageSize, long offset) {
+ List fullSourceTargets = new ArrayList();
+ 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);
+ }
+ }
+ fullSourceTargets.sort(new Comparator() {
+ @Override
+ public int compare(SuggestionTarget arg0, SuggestionTarget arg1) {
+ return -(arg0.getTotal() - arg1.getTotal());
+ }
+ }
+ );
+ // 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());
+ }
+
+ @Override
+ public long countSources(Context context) {
+ return providersMap.size();
+ }
+
+ @Override
+ public SuggestionSource findSource(Context context, String source) {
+ if (providersMap.containsKey(source)) {
+ SuggestionSource ssource = new SuggestionSource(source);
+ ssource.setTotal((int) providersMap.get(source).countAllTargets(context));
+ return ssource;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public List findAllSources(Context context, int pageSize, long offset) {
+ List fullSources = getSources(context).stream().skip(offset).limit(pageSize)
+ .collect(Collectors.toList());
+ return fullSources;
+ }
+
+ private List getSources(Context context) {
+ List results = new ArrayList();
+ for (String source : providersMap.keySet()) {
+ SuggestionSource ssource = new SuggestionSource(source);
+ ssource.setTotal((int) providersMap.get(source).countAllTargets(context));
+ results.add(ssource);
+ }
+ return results;
+ }
+
+ @Override
+ public long countAllByTargetAndSource(Context context, String source, UUID target) {
+ if (providersMap.containsKey(source)) {
+ return providersMap.get(source).countUnprocessedSuggestionByTarget(context, target);
+ }
+ return 0;
+ }
+
+ @Override
+ public List findByTargetAndSource(Context context, UUID target, String source, int pageSize,
+ long offset, boolean ascending) {
+ if (providersMap.containsKey(source)) {
+ return providersMap.get(source).findAllUnprocessedSuggestions(context, target, pageSize, offset, ascending);
+ }
+ return null;
+ }
+
+ @Override
+ public Suggestion findUnprocessedSuggestion(Context context, String id) {
+ String source = null;
+ UUID target = null;
+ String idPart = null;
+ String[] split;
+ try {
+ split = id.split(":", 3);
+ source = split[0];
+ target = UUID.fromString(split[1]);
+ idPart = split[2];
+ } catch (Exception e) {
+ log.warn("findSuggestion got an invalid id " + id + ", return null");
+ return null;
+ }
+ if (split.length != 3) {
+ return null;
+ }
+ if (providersMap.containsKey(source)) {
+ return providersMap.get(source).findUnprocessedSuggestion(context, target, idPart);
+ }
+ return null;
+ }
+
+ @Override
+ public void rejectSuggestion(Context context, String id) {
+ String source = null;
+ UUID target = null;
+ String idPart = null;
+ String[] split;
+ try {
+ split = id.split(":", 3);
+ source = split[0];
+ target = UUID.fromString(split[1]);
+ idPart = split[2];
+ } catch (Exception e) {
+ log.warn("rejectSuggestion got an invalid id " + id + ", doing nothing");
+ return;
+ }
+ if (split.length != 3) {
+ return;
+ }
+ if (providersMap.containsKey(source)) {
+ providersMap.get(source).rejectSuggestion(context, target, idPart);
+ }
+
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionSource.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionSource.java
new file mode 100644
index 000000000000..6dcc3f7e1e4c
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionSource.java
@@ -0,0 +1,49 @@
+/**
+ * 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.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() {
+ }
+
+ /**
+ * Summarize the available suggestions from a source.
+ *
+ * @param name the name must be not null
+ */
+ public SuggestionSource(String name) {
+ super();
+ this.name = name;
+ }
+
+ public String getID() {
+ return name;
+ }
+
+ public int getTotal() {
+ return total;
+ }
+
+ public void setTotal(int total) {
+ this.total = total;
+ }
+
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionTarget.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionTarget.java
new file mode 100644
index 000000000000..db82aa8081ba
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionTarget.java
@@ -0,0 +1,75 @@
+/**
+ * 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.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 repository item (usually a person item) into a suggestion target.
+ *
+ * @param item must be not null
+ */
+ public SuggestionTarget(Item item) {
+ super();
+ this.target = item;
+ }
+
+ /**
+ * The suggestion target uses the concatenation of the source and target uuid separated by colon as id
+ *
+ * @return the source:uuid of the wrapped item
+ */
+ public String getID() {
+ return source + ":" + (target != null ? target.getID() : "");
+ }
+
+ public Item getTarget() {
+ return target;
+ }
+
+ public void setTarget(Item target) {
+ this.target = target;
+ }
+
+ public String getSource() {
+ return source;
+ }
+
+ public void setSource(String source) {
+ this.source = source;
+ }
+
+ public int getTotal() {
+ return total;
+ }
+
+ public void setTotal(int total) {
+ this.total = total;
+ }
+
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionUtils.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionUtils.java
new file mode 100644
index 000000000000..30ced75fc914
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionUtils.java
@@ -0,0 +1,111 @@
+/**
+ * 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.suggestion;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.lang3.StringUtils;
+import org.dspace.external.model.ExternalDataObject;
+
+/**
+ * This utility class provides convenient methods to deal with the
+ * {@link ExternalDataObject} for the purpose of the Suggestion framework
+ *
+ * @author Andrea Bollini (andrea.bollini at 4science.it)
+ */
+public class SuggestionUtils {
+ private SuggestionUtils() {
+ }
+ /**
+ * This method receive an ExternalDataObject and a metadatum key.
+ * It return only the values of the Metadata associated with the key.
+ *
+ * @param record the ExternalDataObject to extract metadata from
+ * @param schema schema of the searching record
+ * @param element element of the searching record
+ * @param qualifier qualifier of the searching record
+ * @return value of the first matching record
+ */
+ public static List getAllEntriesByMetadatum(ExternalDataObject record, String schema, String element,
+ String qualifier) {
+ return record.getMetadata().stream()
+ .filter(x ->
+ StringUtils.equals(x.getSchema(), schema)
+ && StringUtils.equals(x.getElement(), element)
+ && StringUtils.equals(x.getQualifier(), qualifier))
+ .map(x -> x.getValue()).collect(Collectors.toList());
+ }
+
+ /**
+ * This method receive an ExternalDataObject and a metadatum key.
+ * It return only the values of the Metadata associated with the key.
+ *
+ * @param record the ExternalDataObject to extract metadata from
+ * @param metadataFieldKey the metadata field key (i.e. dc.title or dc.contributor.author),
+ * the jolly char is not supported
+ * @return value of the first matching record
+ */
+ public static List getAllEntriesByMetadatum(ExternalDataObject record, String metadataFieldKey) {
+ if (metadataFieldKey == null) {
+ return Collections.EMPTY_LIST;
+ }
+ String[] fields = metadataFieldKey.split("\\.");
+ String schema = fields[0];
+ String element = fields[1];
+ String qualifier = null;
+ if (fields.length == 3) {
+ qualifier = fields[2];
+ }
+ return getAllEntriesByMetadatum(record, schema, element, qualifier);
+ }
+
+ /**
+ * This method receive and ExternalDataObject and a metadatum key.
+ * It return only the value of the first Metadatum from the list associated with the key.
+ *
+ * @param record the ExternalDataObject to extract metadata from
+ * @param schema schema of the searching record
+ * @param element element of the searching record
+ * @param qualifier qualifier of the searching record
+ * @return value of the first matching record
+ */
+ public static String getFirstEntryByMetadatum(ExternalDataObject record, String schema, String element,
+ String qualifier) {
+ return record.getMetadata().stream()
+ .filter(x ->
+ StringUtils.equals(x.getSchema(), schema)
+ && StringUtils.equals(x.getElement(), element)
+ && StringUtils.equals(x.getQualifier(), qualifier))
+ .map(x -> x.getValue()).findFirst().orElse(null);
+ }
+
+ /**
+ * This method receive and ExternalDataObject and a metadatum key.
+ * It return only the value of the first Metadatum from the list associated with the key.
+ *
+ * @param record the ExternalDataObject to extract metadata from
+ * @param metadataFieldKey the metadata field key (i.e. dc.title or dc.contributor.author),
+ * the jolly char is not supported
+ * @return value of the first matching record
+ */
+ public static String getFirstEntryByMetadatum(ExternalDataObject record, String metadataFieldKey) {
+ if (metadataFieldKey == null) {
+ return null;
+ }
+ String[] fields = metadataFieldKey.split("\\.");
+ String schema = fields[0];
+ String element = fields[1];
+ String qualifier = null;
+ if (fields.length == 3) {
+ qualifier = fields[2];
+ }
+ return getFirstEntryByMetadatum(record, schema, element, qualifier);
+ }
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/AuthorNamesScorer.java b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/AuthorNamesScorer.java
new file mode 100644
index 000000000000..60b1521f7eda
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/AuthorNamesScorer.java
@@ -0,0 +1,151 @@
+/**
+ * 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.suggestion.openaire;
+
+import static org.dspace.app.suggestion.SuggestionUtils.getAllEntriesByMetadatum;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import com.ibm.icu.text.CharsetDetector;
+import com.ibm.icu.text.CharsetMatch;
+import com.ibm.icu.text.Normalizer;
+import org.apache.commons.lang3.StringUtils;
+import org.dspace.app.suggestion.SuggestionEvidence;
+import org.dspace.content.Item;
+import org.dspace.content.MetadataValue;
+import org.dspace.content.service.ItemService;
+import org.dspace.external.model.ExternalDataObject;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Implementation of {@see org.dspace.app.suggestion.oaire.EvidenceScorer} which evaluate ImportRecords
+ * based on Author's name.
+ *
+ * @author Andrea Bollini (andrea.bollini at 4science dot it)
+ * @author Pasquale Cavallo (pasquale.cavallo at 4science dot it)
+ *
+ */
+public class AuthorNamesScorer implements EvidenceScorer {
+
+ private List contributorMetadata;
+
+ private List names;
+
+ @Autowired
+ private ItemService itemService;
+
+ /**
+ * returns the metadata key of the Item which to base the filter on
+ * @return metadata key
+ */
+ public List getContributorMetadata() {
+ return contributorMetadata;
+ }
+
+ /**
+ * set the metadata key of the Item which to base the filter on
+ */
+ public void setContributorMetadata(List contributorMetadata) {
+ this.contributorMetadata = contributorMetadata;
+ }
+
+ /**
+ * return the metadata key of ImportRecord which to base the filter on
+ * @return
+ */
+ public List getNames() {
+ return names;
+ }
+
+ /**
+ * set the metadata key of ImportRecord which to base the filter on
+ */
+ public void setNames(List names) {
+ this.names = names;
+ }
+
+ /**
+ * Method which is responsible to evaluate ImportRecord based on authors name.
+ * This method extract the researcher name from Item using contributorMetadata fields
+ * and try to match them with values extract from ImportRecord using metadata keys defined
+ * in names.
+ * ImportRecords which don't match will be discarded.
+ *
+ * @param importRecord the import record to check
+ * @param researcher DSpace item
+ * @return the generated evidence or null if the record must be discarded
+ */
+ @Override
+ public SuggestionEvidence computeEvidence(Item researcher, ExternalDataObject importRecord) {
+ List names = searchMetadataValues(researcher);
+ int maxNameLenght = names.stream().mapToInt(n -> n[0].length()).max().orElse(1);
+ List metadataAuthors = new ArrayList<>();
+ for (String contributorMetadatum : contributorMetadata) {
+ metadataAuthors.addAll(getAllEntriesByMetadatum(importRecord, contributorMetadatum));
+ }
+ List normalizedMetadataAuthors = metadataAuthors.stream().map(x -> normalize(x))
+ .collect(Collectors.toList());
+ int idx = 0;
+ for (String nMetadataAuthor : normalizedMetadataAuthors) {
+ Optional found = names.stream()
+ .filter(a -> StringUtils.equalsIgnoreCase(a[0], nMetadataAuthor)).findFirst();
+ if (found.isPresent()) {
+ return new SuggestionEvidence(this.getClass().getSimpleName(),
+ 100 * ((double) nMetadataAuthor.length() / (double) maxNameLenght),
+ "The author " + metadataAuthors.get(idx) + " at position " + (idx + 1)
+ + " in the authors list matches the name " + found.get()[1]
+ + " in the researcher profile");
+ }
+ idx++;
+ }
+ return null;
+ }
+
+ /**
+ * Return list of Item metadata values starting from metadata keys defined in class level variable names.
+ *
+ * @param researcher DSpace item
+ * @return list of metadata values
+ */
+ private List searchMetadataValues(Item researcher) {
+ List authors = new ArrayList();
+ for (String name : names) {
+ List values = itemService.getMetadataByMetadataString(researcher, name);
+ if (values != null) {
+ for (MetadataValue v : values) {
+ authors.add(new String[] {normalize(v.getValue()), v.getValue()});
+ }
+ }
+ }
+ 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();
+ cd.setText(value.getBytes());
+ CharsetMatch detect = cd.detect();
+ if (detect != null && detect.getLanguage() != null) {
+ norm = norm.replaceAll("[^\\p{L}]", " ").toLowerCase(new Locale(detect.getLanguage()));
+ } else {
+ norm = norm.replaceAll("[^\\p{L}]", " ").toLowerCase();
+ }
+ return Arrays.asList(norm.split("\\s+")).stream().sorted().collect(Collectors.joining());
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/DateScorer.java b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/DateScorer.java
new file mode 100644
index 000000000000..94f81715fa63
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/DateScorer.java
@@ -0,0 +1,214 @@
+/**
+ * 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.suggestion.openaire;
+
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.List;
+
+import org.dspace.app.suggestion.SuggestionEvidence;
+import org.dspace.app.suggestion.SuggestionUtils;
+import org.dspace.content.Item;
+import org.dspace.content.MetadataValue;
+import org.dspace.content.service.ItemService;
+import org.dspace.external.model.ExternalDataObject;
+import org.dspace.util.MultiFormatDateParser;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Implementation of {@see org.dspace.app.suggestion.oaire.EvidenceScorer} which evaluate ImportRecords
+ * based on the distance from a date extracted from the ResearcherProfile (birthday / graduation date)
+ *
+ * @author Andrea Bollini (andrea.bollini at 4science dot it)
+ *
+ */
+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;
+
+ /**
+ * 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) {
+ this.itemService = itemService;
+ }
+
+ public void setBirthDateMetadata(String birthDate) {
+ this.birthDateMetadata = birthDate;
+ }
+
+ public String getBirthDateMetadata() {
+ return birthDateMetadata;
+ }
+
+ public void setEducationDateMetadata(String educationDate) {
+ this.educationDateMetadata = educationDate;
+ }
+
+ public String getEducationDateMetadata() {
+ return educationDateMetadata;
+ }
+
+ public void setBirthDateDelta(int birthDateDelta) {
+ this.birthDateDelta = birthDateDelta;
+ }
+
+ public void setBirthDateRange(int birthDateRange) {
+ this.birthDateRange = birthDateRange;
+ }
+
+ public void setEducationDateDelta(int educationDateDelta) {
+ this.educationDateDelta = educationDateDelta;
+ }
+
+ public void setEducationDateRange(int educationDateRange) {
+ this.educationDateRange = educationDateRange;
+ }
+
+ public void setPublicationDateMetadata(String publicationDateMetadata) {
+ this.publicationDateMetadata = publicationDateMetadata;
+ }
+
+ /**
+ * 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
+ * @return the generated evidence or null if the record must be discarded
+ */
+ @Override
+ public SuggestionEvidence computeEvidence(Item researcher, ExternalDataObject importRecord) {
+ Integer[] range = calculateRange(researcher);
+ if (range == null) {
+ return new SuggestionEvidence(this.getClass().getSimpleName(),
+ 0,
+ "No assumption was possible about the publication year range. "
+ + "Please consider setting your birthday in your profile.");
+ } else {
+ String optDate = SuggestionUtils.getFirstEntryByMetadatum(importRecord, publicationDateMetadata);
+ int year = getYear(optDate);
+ if (year > 0) {
+ if ((range[0] == null || year >= range[0]) &&
+ (range[1] == null || year <= range[1])) {
+ return new SuggestionEvidence(this.getClass().getSimpleName(),
+ 10,
+ "The publication date is within the expected range [" + range[0] + ", "
+ + range[1] + "]");
+ } else {
+ // outside the range, discard the suggestion
+ return null;
+ }
+ } else {
+ return new SuggestionEvidence(this.getClass().getSimpleName(),
+ 0,
+ "No assumption was possible as the publication date is " + (optDate != null
+ ? "unprocessable [" + optDate + "]"
+ : "unknown"));
+ }
+ }
+ }
+
+ /**
+ * 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 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 {
+ return null;
+ }
+ }
+
+ private List getListMetadataValues(Item researcher, String metadataKey) {
+ if (metadataKey != null) {
+ return itemService.getMetadataByMetadataString(researcher, metadataKey);
+ } else {
+ return Collections.EMPTY_LIST;
+ }
+ }
+
+ private String getSingleValue(Item researcher, String metadataKey) {
+ if (metadataKey != null) {
+ return itemService.getMetadata(researcher, metadataKey);
+ }
+ return null;
+ }
+
+ private int getYear(String birthDateStr) {
+ int birthDateYear = -1;
+ if (birthDateStr != null) {
+ Date birthDate = MultiFormatDateParser.parse(birthDateStr);
+ if (birthDate != null) {
+ Calendar calendar = new GregorianCalendar();
+ calendar.setTime(birthDate);
+ birthDateYear = calendar.get(Calendar.YEAR);
+ }
+ }
+ return birthDateYear;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/EvidenceScorer.java b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/EvidenceScorer.java
new file mode 100644
index 000000000000..027e9902f94e
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/EvidenceScorer.java
@@ -0,0 +1,37 @@
+/**
+ * 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.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.PublicationApproverServiceImpl}
+ * to construct filtering pipeline.
+ *
+ * For each EvidenceScorer, the service call computeEvidence method.
+ *
+ * @author Andrea Bollini (andrea.bollini at 4science dot it)
+ * @author Pasquale Cavallo (pasquale.cavallo at 4science dot it)
+ *
+ */
+public interface EvidenceScorer {
+
+ /**
+ * Method to compute the suggestion evidence of an ImportRecord, a null evidence
+ * would lead the record to be discarded.
+ *
+ * @param researcher DSpace item
+ * @param importRecord the record to evaluate
+ * @return the generated suggestion evidence or null if the record should be
+ * discarded
+ */
+ public SuggestionEvidence computeEvidence(Item researcher, ExternalDataObject importRecord);
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoader.java b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoader.java
new file mode 100644
index 000000000000..7ad723af123c
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoader.java
@@ -0,0 +1,256 @@
+/**
+ * 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.suggestion.openaire;
+
+import static org.dspace.app.suggestion.SuggestionUtils.getAllEntriesByMetadatum;
+import static org.dspace.app.suggestion.SuggestionUtils.getFirstEntryByMetadatum;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.dspace.app.suggestion.SolrSuggestionProvider;
+import org.dspace.app.suggestion.Suggestion;
+import org.dspace.app.suggestion.SuggestionEvidence;
+import org.dspace.content.Item;
+import org.dspace.content.dto.MetadataValueDTO;
+import org.dspace.core.Context;
+import org.dspace.external.model.ExternalDataObject;
+import org.dspace.external.provider.ExternalDataProvider;
+import org.dspace.services.ConfigurationService;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Class responsible to load and manage ImportRecords from OpenAIRE
+ *
+ * @author Pasquale Cavallo (pasquale.cavallo at 4science dot it)
+ *
+ */
+public class PublicationLoader extends SolrSuggestionProvider {
+
+ private List names;
+
+ private ExternalDataProvider primaryProvider;
+
+ private List otherProviders;
+
+ @Autowired
+ private ConfigurationService configurationService;
+
+ private List pipeline;
+
+ public void setPrimaryProvider(ExternalDataProvider primaryProvider) {
+ this.primaryProvider = primaryProvider;
+ }
+
+ public void setOtherProviders(List otherProviders) {
+ this.otherProviders = otherProviders;
+ }
+
+ /**
+ * Set the pipeline of Approver
+ * @param pipeline list Approver
+ */
+ public void setPipeline(List pipeline) {
+ this.pipeline = pipeline;
+ }
+
+ /**
+ * This method filter a list of ImportRecords using a pipeline of AuthorNamesApprover
+ * and return a filtered list of ImportRecords.
+ *
+ * @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
+ */
+ public List reduceAndTransform(Item researcher, List importRecords) {
+ List results = new ArrayList<>();
+ for (ExternalDataObject r : importRecords) {
+ boolean skip = false;
+ List evidences = new ArrayList();
+ for (EvidenceScorer authorNameApprover : pipeline) {
+ SuggestionEvidence evidence = authorNameApprover.computeEvidence(researcher, r);
+ if (evidence != null) {
+ evidences.add(evidence);
+ } else {
+ skip = true;
+ break;
+ }
+ }
+ if (!skip) {
+ Suggestion suggestion = translateImportRecordToSuggestion(researcher, r);
+ suggestion.getEvidences().addAll(evidences);
+ results.add(suggestion);
+ }
+ }
+ return results;
+ }
+
+ /**
+ * Save a List of ImportRecord into Solr.
+ * ImportRecord will be translate into a SolrDocument by the method translateImportRecordToSolrDocument.
+ *
+ * @param context the DSpace Context
+ * @param researcher a DSpace Item
+ * @throws SolrServerException
+ * @throws IOException
+ */
+ public void importAuthorRecords(Context context, Item researcher)
+ throws SolrServerException, IOException {
+ int offset = 0;
+ int limit = 10;
+ int loaded = limit;
+ List searchValues = searchMetadataValues(researcher);
+ while (loaded > 0) {
+ List metadata = getImportRecords(searchValues, researcher, offset, limit);
+ if (metadata.isEmpty()) {
+ loaded = 0;
+ continue;
+ }
+ offset += limit;
+ loaded = metadata.size();
+ List records = reduceAndTransform(researcher, metadata);
+ for (Suggestion record : records) {
+ solrSuggestionStorageService.addSuggestion(record, false, false);
+ }
+ }
+ solrSuggestionStorageService.commit();
+ }
+
+ /**
+ * Translate an ImportRecord into a Suggestion
+ * @param item DSpace item
+ * @param record ImportRecord
+ * @return Suggestion
+ */
+ private Suggestion translateImportRecordToSuggestion(Item item, ExternalDataObject record) {
+ String openAireId = record.getId();
+ Suggestion suggestion = new Suggestion(getSourceName(), item, openAireId);
+ suggestion.setDisplay(getFirstEntryByMetadatum(record, "dc", "title", null));
+ suggestion.getMetadata().add(
+ new MetadataValueDTO("dc", "title", null, null, getFirstEntryByMetadatum(record, "dc", "title", null)));
+ suggestion.getMetadata().add(new MetadataValueDTO("dc", "date", "issued", null,
+ getFirstEntryByMetadatum(record, "dc", "date", "issued")));
+ suggestion.getMetadata().add(new MetadataValueDTO("dc", "description", "abstract", null,
+ getFirstEntryByMetadatum(record, "dc", "description", "abstract")));
+ suggestion.setExternalSourceUri(configurationService.getProperty("dspace.server.url")
+ + "/api/integration/externalsources/" + primaryProvider.getSourceIdentifier() + "/entryValues/"
+ + openAireId);
+ for (String o : getAllEntriesByMetadatum(record, "dc", "source", null)) {
+ suggestion.getMetadata().add(new MetadataValueDTO("dc", "source", null, null, o));
+ }
+ for (String o : getAllEntriesByMetadatum(record, "dc", "contributor", "author")) {
+ suggestion.getMetadata().add(new MetadataValueDTO("dc", "contributor", "author", null, o));
+ }
+ return suggestion;
+ }
+
+ public List getNames() {
+ return names;
+ }
+
+ public void setNames(List names) {
+ this.names = names;
+ }
+
+ /**
+ * Load metadata from OpenAIRE using the import service. The service use the value
+ * 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 getImportRecords(List searchValues,
+ Item researcher, int offset, int limit) {
+ List matchingRecords = new ArrayList<>();
+ for (String searchValue : searchValues) {
+ matchingRecords.addAll(
+ primaryProvider.searchExternalDataObjects(searchValue, offset, limit));
+ }
+ List toReturn = removeDuplicates(matchingRecords);
+ return toReturn;
+ }
+
+ /**
+ * This method remove duplicates from importRecords list.
+ * An element is a duplicate if in the list exist another element
+ * with the same value of the metadatum 'dc.identifier.other'
+ *
+ * @param importRecords list of ImportRecord
+ * @return list of ImportRecords without duplicates
+ */
+ private List removeDuplicates(List importRecords) {
+ List filteredRecords = new ArrayList<>();
+ for (ExternalDataObject currentRecord : importRecords) {
+ if (!isDuplicate(currentRecord, filteredRecords)) {
+ filteredRecords.add(currentRecord);
+ }
+ }
+ return filteredRecords;
+ }
+
+
+ /**
+ * Check if the ImportRecord is already present in the list.
+ * The comparison is made on the value of metadatum with key 'dc.identifier.other'
+ *
+ * @param dto An importRecord instance
+ * @param importRecords a list of importRecord
+ * @return true if dto is already present in importRecords, false otherwise
+ */
+ private boolean isDuplicate(ExternalDataObject dto, List importRecords) {
+ String currentItemId = dto.getId();
+ if (currentItemId == null) {
+ return true;
+ }
+ for (ExternalDataObject importRecord : importRecords) {
+ if (currentItemId.equals(importRecord.getId())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ /**
+ * Return list of Item metadata values starting from metadata keys defined in class level variable names.
+ *
+ * @param researcher DSpace item
+ * @return list of metadata values
+ */
+ private List searchMetadataValues(Item researcher) {
+ List authors = new ArrayList();
+ for (String name : names) {
+ String value = itemService.getMetadata(researcher, name);
+ if (value != null) {
+ authors.add(value);
+ }
+ }
+ return authors;
+ }
+
+ @Override
+ protected boolean isExternalDataObjectPotentiallySuggested(Context context, ExternalDataObject externalDataObject) {
+ if (StringUtils.equals(externalDataObject.getSource(), primaryProvider.getSourceIdentifier())) {
+ return true;
+ } else if (otherProviders != null) {
+ return otherProviders.stream()
+ .anyMatch(x -> StringUtils.equals(externalDataObject.getSource(), x.getSourceIdentifier()));
+ } else {
+ return false;
+ }
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderCliScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderCliScriptConfiguration.java
new file mode 100644
index 000000000000..f5289fd99aba
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderCliScriptConfiguration.java
@@ -0,0 +1,29 @@
+/**
+ * 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.suggestion.openaire;
+
+import org.apache.commons.cli.Options;
+
+/**
+ * Extension of {@link PublicationLoaderScriptConfiguration} for CLI.
+ *
+ * @author Alessandro Martelli (alessandro.martelli at 4science.it)
+ */
+public class PublicationLoaderCliScriptConfiguration
+ extends PublicationLoaderScriptConfiguration {
+
+ @Override
+ public Options getOptions() {
+ Options options = super.getOptions();
+ options.addOption("h", "help", false, "help");
+ options.getOption("h").setType(boolean.class);
+ super.options = options;
+ return options;
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderRunnable.java b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderRunnable.java
new file mode 100644
index 000000000000..76e8213cd7d2
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderRunnable.java
@@ -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.app.suggestion.openaire;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.commons.cli.ParseException;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.dspace.content.Item;
+import org.dspace.core.Context;
+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;
+
+/**
+ * Runner responsible to import metadata about authors from OpenAIRE to Solr.
+ * This runner works in two ways:
+ * If -s parameter with a valid UUID is received, then the specific researcher
+ * with this UUID will be used.
+ * Invocation without any parameter results in massive import, processing all
+ * authors registered in DSpace.
+ *
+ * @author Alessandro Martelli (alessandro.martelli at 4science.it)
+ */
+public class PublicationLoaderRunnable
+ extends DSpaceRunnable> {
+
+ private static final Logger LOGGER = LogManager.getLogger();
+
+ private PublicationLoader oairePublicationLoader = null;
+
+ protected Context context;
+
+ protected String profile;
+
+ @Override
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public PublicationLoaderScriptConfiguration getScriptConfiguration() {
+ PublicationLoaderScriptConfiguration configuration = new DSpace().getServiceManager()
+ .getServiceByName("import-openaire-suggestions", PublicationLoaderScriptConfiguration.class);
+ return configuration;
+ }
+
+ @Override
+ public void setup() throws ParseException {
+
+ oairePublicationLoader = new DSpace().getServiceManager().getServiceByName(
+ "OpenairePublicationLoader", PublicationLoader.class);
+
+ profile = commandLine.getOptionValue("s");
+ if (profile == null) {
+ LOGGER.info("No argument for -s, process all profiles");
+ } else {
+ LOGGER.info("Process eperson item with UUID {}", profile);
+ }
+ }
+
+ @Override
+ public void internalRun() throws Exception {
+
+ context = new Context();
+
+ Iterator- researchers = getResearchers(profile);
+ while (researchers.hasNext()) {
+ Item researcher = researchers.next();
+ oairePublicationLoader.importAuthorRecords(context, researcher);
+ }
+
+ }
+
+ /**
+ * Get the Item(s) which map a researcher from Solr. If the uuid is specified,
+ * the researcher with this UUID will be chosen. If the uuid doesn't match any
+ * researcher, the method returns an empty array list. If uuid is null, all
+ * research will be return.
+ *
+ * @param profileUUID uuid of the researcher. If null, all researcher will be
+ * returned.
+ * @return the researcher with specified UUID or all researchers
+ */
+ @SuppressWarnings("rawtypes")
+ private Iterator
- getResearchers(String profileUUID) {
+ SearchService searchService = new DSpace().getSingletonService(SearchService.class);
+ DiscoverQueryBuilder queryBuilder = SearchUtils.getQueryBuilder();
+ List filters = new ArrayList<>();
+ String query = "*:*";
+ if (profileUUID != null) {
+ query = "search.resourceid:" + profileUUID;
+ }
+ 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);
+ }
+ return null;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderRunnableCli.java b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderRunnableCli.java
new file mode 100644
index 000000000000..6c59b725d506
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderRunnableCli.java
@@ -0,0 +1,36 @@
+/**
+ * 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.suggestion.openaire;
+
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.ParseException;
+import org.dspace.utils.DSpace;
+
+public class PublicationLoaderRunnableCli extends PublicationLoaderRunnable {
+
+ @Override
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public PublicationLoaderCliScriptConfiguration getScriptConfiguration() {
+ PublicationLoaderCliScriptConfiguration configuration = new DSpace().getServiceManager()
+ .getServiceByName("import-openaire-suggestions", PublicationLoaderCliScriptConfiguration.class);
+ return configuration;
+ }
+
+ @Override
+ public void setup() throws ParseException {
+ super.setup();
+
+ // in case of CLI we show the help prompt
+ if (commandLine.hasOption('h')) {
+ HelpFormatter formatter = new HelpFormatter();
+ formatter.printHelp("Import Researchers Suggestions", getScriptConfiguration().getOptions());
+ System.exit(0);
+ }
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderScriptConfiguration.java
new file mode 100644
index 000000000000..8bef7de40d29
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderScriptConfiguration.java
@@ -0,0 +1,56 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.app.suggestion.openaire;
+
+import org.apache.commons.cli.Options;
+import org.dspace.scripts.configuration.ScriptConfiguration;
+
+public class PublicationLoaderScriptConfiguration
+ extends ScriptConfiguration {
+
+ private Class dspaceRunnableClass;
+
+ @Override
+ public Class