Merge branch 'main' into task/main/CST-15074

# Conflicts:
#	dspace-api/src/main/java/org/dspace/orcid/service/impl/OrcidSynchronizationServiceImpl.java
#	dspace-api/src/test/java/org/dspace/identifier/VersionedHandleIdentifierProviderIT.java
This commit is contained in:
Vincenzo Mecca
2025-03-06 19:24:00 +01:00
284 changed files with 8857 additions and 2741 deletions

341
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,341 @@
#-------------------
# DSpace's dependabot rules. Enables maven updates for all dependencies on a weekly basis
# for main and any maintenance branches. Security updates only apply to main.
#-------------------
version: 2
updates:
###############
## Main branch
###############
# NOTE: At this time, "security-updates" rules only apply if "target-branch" is unspecified
# So, only this first section can include "applies-to: security-updates"
- package-ecosystem: "maven"
directory: "/"
schedule:
interval: "weekly"
# Allow up to 10 open PRs for dependencies
open-pull-requests-limit: 10
# Group together some upgrades in a single PR
groups:
# Group together all Build Tools in a single PR
build-tools:
applies-to: version-updates
patterns:
- "org.apache.maven.plugins:*"
- "*:*-maven-plugin"
- "*:maven-*-plugin"
- "com.github.spotbugs:spotbugs"
- "com.google.code.findbugs:*"
- "com.google.errorprone:*"
- "com.puppycrawl.tools:checkstyle"
- "org.sonatype.plugins:*"
exclude-patterns:
# Exclude anything from Spring, as that is in a separate group
- "org.springframework.*:*"
update-types:
- "minor"
- "patch"
test-tools:
applies-to: version-updates
patterns:
- "junit:*"
- "com.github.stefanbirker:system-rules"
- "com.h2database:*"
- "io.findify:s3mock*"
- "io.netty:*"
- "org.hamcrest:*"
- "org.mock-server:*"
- "org.mockito:*"
update-types:
- "minor"
- "patch"
# Group together all Apache Commons deps in a single PR
apache-commons:
applies-to: version-updates
patterns:
- "org.apache.commons:*"
- "commons-*:commons-*"
update-types:
- "minor"
- "patch"
# Group together all fasterxml deps in a single PR
fasterxml:
applies-to: version-updates
patterns:
- "com.fasterxml:*"
- "com.fasterxml.*:*"
update-types:
- "minor"
- "patch"
# Group together all Hibernate deps in a single PR
hibernate:
applies-to: version-updates
patterns:
- "org.hibernate.*:*"
update-types:
- "minor"
- "patch"
# Group together all Jakarta deps in a single PR
jakarta:
applies-to: version-updates
patterns:
- "jakarta.*:*"
- "org.eclipse.angus:jakarta.mail"
- "org.glassfish.jaxb:jaxb-runtime"
update-types:
- "minor"
- "patch"
# Group together all Spring deps in a single PR
spring:
applies-to: version-updates
patterns:
- "org.springframework:*"
- "org.springframework.*:*"
update-types:
- "minor"
- "patch"
# Group together all WebJARs deps in a single PR
webjars:
applies-to: version-updates
patterns:
- "org.webjars:*"
- "org.webjars.*:*"
update-types:
- "minor"
- "patch"
ignore:
# Don't try to auto-update any DSpace dependencies
- dependency-name: "org.dspace:*"
- dependency-name: "org.dspace.*:*"
# Ignore all major version updates for all dependencies. We'll only automate minor/patch updates.
- dependency-name: "*"
update-types: ["version-update:semver-major"]
######################
## dspace-8_x branch
######################
- package-ecosystem: "maven"
directory: "/"
target-branch: dspace-8_x
schedule:
interval: "weekly"
# Allow up to 10 open PRs for dependencies
open-pull-requests-limit: 10
# Group together some upgrades in a single PR
groups:
# Group together all Build Tools in a single PR
build-tools:
applies-to: version-updates
patterns:
- "org.apache.maven.plugins:*"
- "*:*-maven-plugin"
- "*:maven-*-plugin"
- "com.github.spotbugs:spotbugs"
- "com.google.code.findbugs:*"
- "com.google.errorprone:*"
- "com.puppycrawl.tools:checkstyle"
- "org.sonatype.plugins:*"
exclude-patterns:
# Exclude anything from Spring, as that is in a separate group
- "org.springframework.*:*"
update-types:
- "minor"
- "patch"
test-tools:
applies-to: version-updates
patterns:
- "junit:*"
- "com.github.stefanbirker:system-rules"
- "com.h2database:*"
- "io.findify:s3mock*"
- "io.netty:*"
- "org.hamcrest:*"
- "org.mock-server:*"
- "org.mockito:*"
update-types:
- "minor"
- "patch"
# Group together all Apache Commons deps in a single PR
apache-commons:
applies-to: version-updates
patterns:
- "org.apache.commons:*"
- "commons-*:commons-*"
update-types:
- "minor"
- "patch"
# Group together all fasterxml deps in a single PR
fasterxml:
applies-to: version-updates
patterns:
- "com.fasterxml:*"
- "com.fasterxml.*:*"
update-types:
- "minor"
- "patch"
# Group together all Hibernate deps in a single PR
hibernate:
applies-to: version-updates
patterns:
- "org.hibernate.*:*"
update-types:
- "minor"
- "patch"
# Group together all Jakarta deps in a single PR
jakarta:
applies-to: version-updates
patterns:
- "jakarta.*:*"
- "org.eclipse.angus:jakarta.mail"
- "org.glassfish.jaxb:jaxb-runtime"
update-types:
- "minor"
- "patch"
# Group together all Spring deps in a single PR
spring:
applies-to: version-updates
patterns:
- "org.springframework:*"
- "org.springframework.*:*"
update-types:
- "minor"
- "patch"
# Group together all WebJARs deps in a single PR
webjars:
applies-to: version-updates
patterns:
- "org.webjars:*"
- "org.webjars.*:*"
update-types:
- "minor"
- "patch"
ignore:
# Don't try to auto-update any DSpace dependencies
- dependency-name: "org.dspace:*"
- dependency-name: "org.dspace.*:*"
# Ignore all major version updates for all dependencies. We'll only automate minor/patch updates.
- dependency-name: "*"
update-types: [ "version-update:semver-major" ]
######################
## dspace-7_x branch
######################
- package-ecosystem: "maven"
directory: "/"
target-branch: dspace-7_x
schedule:
interval: "weekly"
# Allow up to 10 open PRs for dependencies
open-pull-requests-limit: 10
# Group together some upgrades in a single PR
groups:
# Group together all Build Tools in a single PR
build-tools:
applies-to: version-updates
patterns:
- "org.apache.maven.plugins:*"
- "*:*-maven-plugin"
- "*:maven-*-plugin"
- "com.github.spotbugs:spotbugs"
- "com.google.code.findbugs:*"
- "com.google.errorprone:*"
- "com.puppycrawl.tools:checkstyle"
- "org.sonatype.plugins:*"
exclude-patterns:
# Exclude anything from Spring, as that is in a separate group
- "org.springframework.*:*"
update-types:
- "minor"
- "patch"
test-tools:
applies-to: version-updates
patterns:
- "junit:*"
- "com.github.stefanbirker:system-rules"
- "com.h2database:*"
- "io.findify:s3mock*"
- "io.netty:*"
- "org.hamcrest:*"
- "org.mock-server:*"
- "org.mockito:*"
update-types:
- "minor"
- "patch"
# Group together all Apache Commons deps in a single PR
apache-commons:
applies-to: version-updates
patterns:
- "org.apache.commons:*"
- "commons-*:commons-*"
update-types:
- "minor"
- "patch"
# Group together all fasterxml deps in a single PR
fasterxml:
applies-to: version-updates
patterns:
- "com.fasterxml:*"
- "com.fasterxml.*:*"
update-types:
- "minor"
- "patch"
# Group together all Hibernate deps in a single PR
hibernate:
applies-to: version-updates
patterns:
- "org.hibernate.*:*"
update-types:
- "minor"
- "patch"
# Group together all Jakarta deps in a single PR
jakarta:
applies-to: version-updates
patterns:
- "jakarta.*:*"
- "org.eclipse.angus:jakarta.mail"
- "org.glassfish.jaxb:jaxb-runtime"
update-types:
- "minor"
- "patch"
# Group together all Google deps in a single PR
# NOTE: These Google deps are only used in 7.x and have been removed in 8.x and later
google-apis:
applies-to: version-updates
patterns:
- "com.google.apis:*"
- "com.google.api-client:*"
- "com.google.http-client:*"
- "com.google.oauth-client:*"
update-types:
- "minor"
- "patch"
# Group together all Spring deps in a single PR
spring:
applies-to: version-updates
patterns:
- "org.springframework:*"
- "org.springframework.*:*"
update-types:
- "minor"
- "patch"
# Group together all WebJARs deps in a single PR
webjars:
applies-to: version-updates
patterns:
- "org.webjars:*"
- "org.webjars.*:*"
update-types:
- "minor"
- "patch"
ignore:
# Don't try to auto-update any DSpace dependencies
- dependency-name: "org.dspace:*"
- dependency-name: "org.dspace.*:*"
# Last version of errorprone to support JDK 11 is 2.31.0
- dependency-name: "com.google.errorprone:*"
versions: [">=2.32.0"]
# Spring Security 5.8 changes the behavior of CSRF Tokens in a way which is incompatible with DSpace 7
# See https://github.com/DSpace/DSpace/pull/9888#issuecomment-2408165545
- dependency-name: "org.springframework.security:*"
versions: [">=5.8.0"]
# Ignore all major version updates for all dependencies. We'll only automate minor/patch updates.
- dependency-name: "*"
update-types: [ "version-update:semver-major" ]

View File

@@ -15,6 +15,7 @@ on:
permissions:
contents: read # to fetch code (actions/checkout)
packages: write # to write images to GitHub Container Registry (GHCR)
jobs:
####################################################
@@ -148,3 +149,101 @@ jobs:
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }}
#################################################################################
# Test Deployment via Docker to ensure newly built images are working properly
#################################################################################
docker-deploy:
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace'
if: github.repository == 'dspace/dspace'
runs-on: ubuntu-latest
# Must run after all major images are built
needs: [dspace, dspace-test, dspace-cli, dspace-postgres-pgcrypto, dspace-solr]
env:
# Override defaults dspace.server.url because backend starts at http://127.0.0.1:8080
dspace__P__server__P__url: http://127.0.0.1:8080/server
# Enable all optional modules / controllers for this test deployment.
# This helps check for errors in deploying these modules via Spring Boot
iiif__P__enabled: true
ldn__P__enabled: true
oai__P__enabled: true
rdf__P__enabled: true
signposting__P__enabled: true
sword__D__server__P__enabled: true
swordv2__D__server__P__enabled: true
# If this is a PR against main (default branch), use "latest".
# Else if this is a PR against a different branch, used the base branch name.
# Else if this is a commit on main (default branch), use the "latest" tag.
# Else, just use the branch name.
# NOTE: DSPACE_VER is used because our docker compose scripts default to using the "-test" image.
DSPACE_VER: ${{ (github.event_name == 'pull_request' && github.event.pull_request.base.ref == github.event.repository.default_branch && 'latest') || (github.event_name == 'pull_request' && github.event.pull_request.base.ref) || (github.ref_name == github.event.repository.default_branch && 'latest') || github.ref_name }}
# Docker Registry to use for Docker compose scripts below.
# We use GitHub's Container Registry to avoid aggressive rate limits at DockerHub.
DOCKER_REGISTRY: ghcr.io
steps:
# Checkout our codebase (to get access to Docker Compose scripts)
- name: Checkout codebase
uses: actions/checkout@v4
# Download Docker image artifacts (which were just built by reusable-docker-build.yml)
- name: Download Docker image artifacts
uses: actions/download-artifact@v4
with:
# Download all amd64 Docker images (TAR files) into the /tmp/docker directory
pattern: docker-image-*-linux-amd64
path: /tmp/docker
merge-multiple: true
# Load each of the images into Docker by calling "docker image load" for each.
# This ensures we are using the images just built & not any prior versions on DockerHub
- name: Load all downloaded Docker images
run: |
find /tmp/docker -type f -name "*.tar" -exec docker image load --input "{}" \;
docker image ls -a
# Start backend using our compose script in the codebase.
- name: Start backend in Docker
run: |
docker compose -f docker-compose.yml up -d
sleep 10
docker container ls
# Create a test admin account. Load test data from a simple set of AIPs as defined in cli.ingest.yml
- name: Load test data into Backend
run: |
docker compose -f docker-compose-cli.yml run --rm dspace-cli create-administrator -e test@test.edu -f admin -l user -p admin -c en
docker compose -f docker-compose-cli.yml -f dspace/src/main/docker-compose/cli.ingest.yml run --rm dspace-cli
# Verify backend started successfully.
# 1. Make sure root endpoint is responding (check for dspace.name defined in docker-compose.yml)
# 2. Also check /collections endpoint to ensure the test data loaded properly (check for a collection name in AIPs)
- name: Verify backend is responding properly
run: |
result=$(wget -O- -q http://127.0.0.1:8080/server/api)
echo "$result"
echo "$result" | grep -oE "\"DSpace Started with Docker Compose\","
result=$(wget -O- -q http://127.0.0.1:8080/server/api/core/collections)
echo "$result"
echo "$result" | grep -oE "\"Dog in Yard\","
# Verify Handle Server can be stared and is working properly
# 1. First generate the "[dspace]/handle-server" folder with the sitebndl.zip
# 2. Start the Handle Server (and wait 20 seconds to let it start up)
# 3. Verify logs do NOT include "Exception" in the text (as that means an error occurred)
# 4. Check that Handle Proxy HTML page is responding on default port (8000)
- name: Verify Handle Server is working properly
run: |
docker exec -i dspace /dspace/bin/make-handle-config
echo "Starting Handle Server..."
docker exec -i dspace /dspace/bin/start-handle-server
sleep 20
echo "Checking for errors in error.log"
result=$(docker exec -i dspace sh -c "cat /dspace/handle-server/logs/error.log* || echo ''")
echo "$result"
echo "$result" | grep -vqz "Exception"
echo "Checking for errors in handle-server.log..."
result=$(docker exec -i dspace cat /dspace/log/handle-server.log)
echo "$result"
echo "$result" | grep -vqz "Exception"
echo "Checking to see if Handle Proxy webpage is available..."
result=$(wget -O- -q http://127.0.0.1:8000/)
echo "$result"
echo "$result" | grep -oE "Handle Proxy"
# Shutdown our containers
- name: Shutdown Docker containers
run: |
docker compose -f docker-compose.yml down

View File

@@ -54,10 +54,13 @@ env:
# For a new commit on default branch (main), use the literal tag 'latest' on Docker image.
# For a new commit on other branches, use the branch name as the tag for Docker image.
# For a new tag, copy that tag name as the tag for Docker image.
# For a pull request, use the name of the base branch that the PR was created against or "latest" (for main).
# e.g. PR against 'main' will use "latest". a PR against 'dspace-7_x' will use 'dspace-7_x'.
IMAGE_TAGS: |
type=raw,value=latest,enable=${{ github.ref_name == github.event.repository.default_branch }}
type=ref,event=branch,enable=${{ github.ref_name != github.event.repository.default_branch }}
type=ref,event=tag
type=raw,value=${{ (github.event.pull_request.base.ref == github.event.repository.default_branch && 'latest') || github.event.pull_request.base.ref }},enable=${{ github.event_name == 'pull_request' }}
# Define default tag "flavor" for docker/metadata-action per
# https://github.com/docker/metadata-action#flavor-input
# We manage the 'latest' tag ourselves to the 'main' branch (see settings above)
@@ -72,6 +75,9 @@ env:
DEPLOY_DEMO_BRANCH: 'dspace-8_x'
DEPLOY_SANDBOX_BRANCH: 'main'
DEPLOY_ARCH: 'linux/amd64'
# Registry used during building of Docker images. (All images are later copied to docker.io registry)
# We use GitHub's Container Registry to avoid aggressive rate limits at DockerHub.
DOCKER_BUILD_REGISTRY: ghcr.io
jobs:
docker-build:
@@ -96,6 +102,7 @@ jobs:
# This step converts the slashes in the "arch" matrix values above into dashes & saves to env.ARCH_NAME
# E.g. "linux/amd64" becomes "linux-amd64"
# This is necessary because all upload artifacts CANNOT have special chars (like slashes)
# NOTE: The regex-like syntax below is Bash Parameter Substitution
- name: Prepare
run: |
platform=${{ matrix.arch }}
@@ -105,35 +112,45 @@ jobs:
- name: Checkout codebase
uses: actions/checkout@v4
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
# https://github.com/docker/login-action
# NOTE: This login occurs for BOTH non-PRs or PRs. PRs *must* also login to access private images from GHCR
# during the build process
- name: Login to ${{ env.DOCKER_BUILD_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKER_BUILD_REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
# https://github.com/docker/setup-qemu-action
- name: Set up QEMU emulation to build for multiple architectures
uses: docker/setup-qemu-action@v3
# https://github.com/docker/login-action
- name: Login to DockerHub
# Only login if not a PR, as PRs only trigger a Docker build and not a push
if: ${{ ! matrix.isPr }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
# https://github.com/docker/metadata-action
# Get Metadata for docker_build_deps step below
- name: Sync metadata (tags, labels) from GitHub to Docker for image
# Extract metadata used for Docker images in all build steps below
- name: Extract metadata (tags, labels) from GitHub for Docker image
id: meta_build
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
images: ${{ env.DOCKER_BUILD_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: ${{ env.IMAGE_TAGS }}
flavor: ${{ env.TAGS_FLAVOR }}
#--------------------------------------------------------------------
# First, for all branch commits (non-PRs) we build the image & upload
# to GitHub Container Registry (GHCR). After uploading the image
# to GHCR, we store the image digest in an artifact, so we can
# create a merged manifest later (see 'docker-build_manifest' job).
#
# NOTE: We use GHCR in order to avoid aggressive rate limits at DockerHub.
#--------------------------------------------------------------------
# https://github.com/docker/build-push-action
- name: Build and push image
- name: Build and push image to ${{ env.DOCKER_BUILD_REGISTRY }}
if: ${{ ! matrix.isPr }}
id: docker_build
uses: docker/build-push-action@v5
with:
@@ -141,15 +158,20 @@ jobs:
${{ inputs.dockerfile_additional_contexts }}
context: ${{ inputs.dockerfile_context }}
file: ${{ inputs.dockerfile_path }}
# Tell DSpace's Docker files to use the build registry instead of DockerHub
build-args:
DOCKER_REGISTRY=${{ env.DOCKER_BUILD_REGISTRY }}
platforms: ${{ matrix.arch }}
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
# but we ONLY do an image push to DockerHub if it's NOT a PR
push: ${{ ! matrix.isPr }}
push: true
# Use tags / labels provided by 'docker/metadata-action' above
tags: ${{ steps.meta_build.outputs.tags }}
labels: ${{ steps.meta_build.outputs.labels }}
# Use GitHub cache to load cached Docker images and cache the results of this build
# This decreases the number of images we need to fetch from DockerHub
cache-from: type=gha,scope=${{ inputs.build_id }}
cache-to: type=gha,scope=${{ inputs.build_id }},mode=max
# Export the digest of Docker build locally (for non PRs only)
# Export the digest of Docker build locally
- name: Export Docker build digest
if: ${{ ! matrix.isPr }}
run: |
@@ -157,7 +179,8 @@ jobs:
digest="${{ steps.docker_build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
# Upload digest to an artifact, so that it can be used in manifest below
# Upload digest to an artifact, so that it can be used in combined manifest below
# (The purpose of the combined manifest is to list both amd64 and arm64 builds under same tag)
- name: Upload Docker build digest to artifact
if: ${{ ! matrix.isPr }}
uses: actions/upload-artifact@v4
@@ -167,33 +190,60 @@ jobs:
if-no-files-found: error
retention-days: 1
# If this build is NOT a PR and passed in a REDEPLOY_SANDBOX_URL secret,
# Then redeploy https://sandbox.dspace.org if this build is for our deployment architecture and 'main' branch.
- name: Redeploy sandbox.dspace.org (based on main branch)
if: |
!matrix.isPR &&
env.REDEPLOY_SANDBOX_URL != '' &&
matrix.arch == env.DEPLOY_ARCH &&
github.ref_name == env.DEPLOY_SANDBOX_BRANCH
run: |
curl -X POST $REDEPLOY_SANDBOX_URL
#------------------------------------------------------------------------------
# Second, we build the image again in order to store it in a local TAR file.
# This TAR of the image is cached/saved as an artifact, so that it can be used
# by later jobs to install the brand-new images for automated testing.
# This TAR build is performed BOTH for PRs and for branch commits (non-PRs).
#
# (This approach has the advantage of avoiding having to download the newly built
# image from DockerHub or GHCR during automated testing.)
#
# See the 'docker-deploy' job in docker.yml as an example of where this TAR is used.
#-------------------------------------------------------------------------------
# Build local image (again) and store in a TAR file in /tmp directory
# This step is only done for AMD64, as that's the only image we use in our automated testing (at this time).
# NOTE: This step cannot be combined with the build above as it's a different type of output.
- name: Build and push image to local TAR file
if: ${{ matrix.arch == 'linux/amd64'}}
uses: docker/build-push-action@v5
with:
build-contexts: |
${{ inputs.dockerfile_additional_contexts }}
context: ${{ inputs.dockerfile_context }}
file: ${{ inputs.dockerfile_path }}
# Tell DSpace's Docker files to use the build registry instead of DockerHub
build-args:
DOCKER_REGISTRY=${{ env.DOCKER_BUILD_REGISTRY }}
platforms: ${{ matrix.arch }}
tags: ${{ steps.meta_build.outputs.tags }}
labels: ${{ steps.meta_build.outputs.labels }}
# Use GitHub cache to load cached Docker images and cache the results of this build
# This decreases the number of images we need to fetch from DockerHub
cache-from: type=gha,scope=${{ inputs.build_id }}
cache-to: type=gha,scope=${{ inputs.build_id }},mode=max
# Export image to a local TAR file
outputs: type=docker,dest=/tmp/${{ inputs.build_id }}.tar
# If this build is NOT a PR and passed in a REDEPLOY_DEMO_URL secret,
# Then redeploy https://demo.dspace.org if this build is for our deployment architecture and demo branch.
- name: Redeploy demo.dspace.org (based on maintenance branch)
if: |
!matrix.isPR &&
env.REDEPLOY_DEMO_URL != '' &&
matrix.arch == env.DEPLOY_ARCH &&
github.ref_name == env.DEPLOY_DEMO_BRANCH
run: |
curl -X POST $REDEPLOY_DEMO_URL
# Upload the local docker image (in TAR file) to a build Artifact
# This step is only done for AMD64, as that's the only image we use in our automated testing (at this time).
- name: Upload local image TAR to artifact
if: ${{ matrix.arch == 'linux/amd64'}}
uses: actions/upload-artifact@v4
with:
name: docker-image-${{ inputs.build_id }}-${{ env.ARCH_NAME }}
path: /tmp/${{ inputs.build_id }}.tar
if-no-files-found: error
retention-days: 1
# Merge Docker digests (from various architectures) into a manifest.
# This runs after all Docker builds complete above, and it tells hub.docker.com
# that these builds should be all included in the manifest for this tag.
# (e.g. AMD64 and ARM64 should be listed as options under the same tagged Docker image)
##########################################################################################
# Merge Docker digests (from various architectures) into a single manifest.
# This runs after all Docker builds complete above. The purpose is to include all builds
# under a single manifest for this tag.
# (e.g. both linux/amd64 and linux/arm64 should be listed under the same tagged Docker image)
##########################################################################################
docker-build_manifest:
# Only run if this is NOT a PR
if: ${{ github.event_name != 'pull_request' }}
runs-on: ubuntu-latest
needs:
@@ -207,29 +257,102 @@ jobs:
pattern: digests-${{ inputs.build_id }}-*
merge-multiple: true
- name: Login to ${{ env.DOCKER_BUILD_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKER_BUILD_REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Add Docker metadata for image
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKER_BUILD_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: ${{ env.IMAGE_TAGS }}
flavor: ${{ env.TAGS_FLAVOR }}
- name: Create manifest list from digests and push to ${{ env.DOCKER_BUILD_REGISTRY }}
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.DOCKER_BUILD_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
- name: Inspect manifest in ${{ env.DOCKER_BUILD_REGISTRY }}
run: |
docker buildx imagetools inspect ${{ env.DOCKER_BUILD_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
##########################################################################################
# Copy images / manifest to DockerHub.
# This MUST run after *both* images (AMD64 and ARM64) are built and uploaded to GitHub
# Container Registry (GHCR). Attempting to run this in parallel to GHCR builds can result
# in a race condition...i.e. the copy to DockerHub may fail if GHCR image has been updated
# at the moment when the copy occurs.
##########################################################################################
docker-copy_to_dockerhub:
# Only run if this is NOT a PR
if: ${{ github.event_name != 'pull_request' }}
runs-on: ubuntu-latest
needs:
- docker-build_manifest
steps:
# 'regctl' is used to more easily copy the image to DockerHub and obtain the digest from DockerHub
# See https://github.com/regclient/regclient/blob/main/docs/regctl.md
- name: Install regctl for Docker registry tools
uses: regclient/actions/regctl-installer@main
with:
release: 'v0.8.0'
# This recreates Docker tags for DockerHub
- name: Add Docker metadata for image
id: meta_dockerhub
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
tags: ${{ env.IMAGE_TAGS }}
flavor: ${{ env.TAGS_FLAVOR }}
- name: Login to Docker Hub
# Login to source registry first, as this is where we are copying *from*
- name: Login to ${{ env.DOCKER_BUILD_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKER_BUILD_REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
# Login to DockerHub, since this is where we are copying *to*
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
- name: Create manifest list from digests and push
working-directory: /tmp/digests
# Copy the image from source to DockerHub
- name: Copy image from ${{ env.DOCKER_BUILD_REGISTRY }} to docker.io
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.IMAGE_NAME }}@sha256:%s ' *)
regctl image copy ${{ env.DOCKER_BUILD_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta_dockerhub.outputs.version }} docker.io/${{ env.IMAGE_NAME }}:${{ steps.meta_dockerhub.outputs.version }}
- name: Inspect image
#--------------------------------------------------------------------
# Finally, check whether demo.dspace.org or sandbox.dspace.org need
# to be redeployed based on these new DockerHub images.
#--------------------------------------------------------------------
# If this build is for the branch that Sandbox uses and passed in a REDEPLOY_SANDBOX_URL secret,
# Then redeploy https://sandbox.dspace.org
- name: Redeploy sandbox.dspace.org (based on main branch)
if: |
env.REDEPLOY_SANDBOX_URL != '' &&
github.ref_name == env.DEPLOY_SANDBOX_BRANCH
run: |
docker buildx imagetools inspect ${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
curl -X POST $REDEPLOY_SANDBOX_URL
# If this build is for the branch that Demo uses and passed in a REDEPLOY_DEMO_URL secret,
# Then redeploy https://demo.dspace.org
- name: Redeploy demo.dspace.org (based on maintenance branch)
if: |
env.REDEPLOY_DEMO_URL != '' &&
github.ref_name == env.DEPLOY_DEMO_BRANCH
run: |
curl -X POST $REDEPLOY_DEMO_URL

4
.gitignore vendored
View File

@@ -10,6 +10,7 @@ tags
.project
.classpath
.checkstyle
.factorypath
## Ignore project files created by IntelliJ IDEA
*.iml
@@ -27,6 +28,9 @@ nbdist/
nbactions.xml
nb-configuration.xml
## Ignore project files created by Visual Studio Code
.vscode/
## Ignore all *.properties file in root folder, EXCEPT build.properties (the default)
## KEPT FOR BACKWARDS COMPATIBILITY WITH 5.x (build.properties is now replaced with local.cfg)
/*.properties

View File

@@ -6,10 +6,14 @@
# This Dockerfile uses JDK17 by default.
# To build with other versions, use "--build-arg JDK_VERSION=[value]"
ARG JDK_VERSION=17
# The Docker version tag to build from
ARG DSPACE_VERSION=latest
# The Docker registry to use for DSpace images. Defaults to "docker.io"
# NOTE: non-DSpace images are hardcoded to use "docker.io" and are not impacted by this build argument
ARG DOCKER_REGISTRY=docker.io
# Step 1 - Run Maven Build
FROM dspace/dspace-dependencies:${DSPACE_VERSION} AS build
FROM ${DOCKER_REGISTRY}/dspace/dspace-dependencies:${DSPACE_VERSION} AS build
ARG TARGET_DIR=dspace-installer
WORKDIR /app
# The dspace-installer directory will be written to /install
@@ -31,35 +35,38 @@ RUN mvn --no-transfer-progress package ${MAVEN_FLAGS} && \
RUN rm -rf /install/webapps/server/
# Step 2 - Run Ant Deploy
FROM eclipse-temurin:${JDK_VERSION} AS ant_build
FROM docker.io/eclipse-temurin:${JDK_VERSION} AS ant_build
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
# Create the initial install deployment using ANT
ENV ANT_VERSION 1.10.13
ENV ANT_HOME /tmp/ant-$ANT_VERSION
ENV PATH $ANT_HOME/bin:$PATH
# Need wget to install ant
RUN apt-get update \
&& apt-get install -y --no-install-recommends wget \
&& apt-get purge -y --auto-remove \
&& rm -rf /var/lib/apt/lists/*
ENV ANT_VERSION=1.10.13
ENV ANT_HOME=/tmp/ant-$ANT_VERSION
ENV PATH=$ANT_HOME/bin:$PATH
# Download and install 'ant'
RUN mkdir $ANT_HOME && \
wget -qO- "https://archive.apache.org/dist/ant/binaries/apache-ant-$ANT_VERSION-bin.tar.gz" | tar -zx --strip-components=1 -C $ANT_HOME
curl --silent --show-error --location --fail --retry 5 --output /tmp/apache-ant.tar.gz \
https://archive.apache.org/dist/ant/binaries/apache-ant-${ANT_VERSION}-bin.tar.gz && \
tar -zx --strip-components=1 -f /tmp/apache-ant.tar.gz -C $ANT_HOME && \
rm /tmp/apache-ant.tar.gz
# Run necessary 'ant' deploy scripts
RUN ant init_installation update_configs update_code update_webapps
# Step 3 - Start up DSpace via Runnable JAR
FROM eclipse-temurin:${JDK_VERSION}
FROM docker.io/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
WORKDIR $DSPACE_INSTALL
# Expose Tomcat port
EXPOSE 8080
# Need host command for "[dspace]/bin/make-handle-config"
RUN apt-get update \
&& apt-get install -y --no-install-recommends host \
&& apt-get purge -y --auto-remove \
&& rm -rf /var/lib/apt/lists/*
# Expose Tomcat port (8080) & Handle Server HTTP port (8000)
EXPOSE 8080 8000
# Give java extra memory (2GB)
ENV JAVA_OPTS=-Xmx2000m
# On startup, run DSpace Runnable JAR

View File

@@ -6,10 +6,14 @@
# This Dockerfile uses JDK17 by default.
# To build with other versions, use "--build-arg JDK_VERSION=[value]"
ARG JDK_VERSION=17
# The Docker version tag to build from
ARG DSPACE_VERSION=latest
# The Docker registry to use for DSpace images. Defaults to "docker.io"
# NOTE: non-DSpace images are hardcoded to use "docker.io" and are not impacted by this build argument
ARG DOCKER_REGISTRY=docker.io
# Step 1 - Run Maven Build
FROM dspace/dspace-dependencies:${DSPACE_VERSION} AS build
FROM ${DOCKER_REGISTRY}/dspace/dspace-dependencies:${DSPACE_VERSION} AS build
ARG TARGET_DIR=dspace-installer
WORKDIR /app
# The dspace-installer directory will be written to /install
@@ -25,28 +29,26 @@ RUN mvn --no-transfer-progress package && \
mvn clean
# Step 2 - Run Ant Deploy
FROM eclipse-temurin:${JDK_VERSION} AS ant_build
FROM docker.io/eclipse-temurin:${JDK_VERSION} AS ant_build
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
# Create the initial install deployment using ANT
ENV ANT_VERSION 1.10.13
ENV ANT_HOME /tmp/ant-$ANT_VERSION
ENV PATH $ANT_HOME/bin:$PATH
# Need wget to install ant
RUN apt-get update \
&& apt-get install -y --no-install-recommends wget \
&& apt-get purge -y --auto-remove \
&& rm -rf /var/lib/apt/lists/*
ENV ANT_VERSION=1.10.13
ENV ANT_HOME=/tmp/ant-$ANT_VERSION
ENV PATH=$ANT_HOME/bin:$PATH
# Download and install 'ant'
RUN mkdir $ANT_HOME && \
wget -qO- "https://archive.apache.org/dist/ant/binaries/apache-ant-$ANT_VERSION-bin.tar.gz" | tar -zx --strip-components=1 -C $ANT_HOME
curl --silent --show-error --location --fail --retry 5 --output /tmp/apache-ant.tar.gz \
https://archive.apache.org/dist/ant/binaries/apache-ant-${ANT_VERSION}-bin.tar.gz && \
tar -zx --strip-components=1 -f /tmp/apache-ant.tar.gz -C $ANT_HOME && \
rm /tmp/apache-ant.tar.gz
# Run necessary 'ant' deploy scripts
RUN ant init_installation update_configs update_code
# Step 3 - Run jdk
FROM eclipse-temurin:${JDK_VERSION}
FROM docker.io/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

View File

@@ -6,8 +6,8 @@
# 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
# Step 1 - Download all Dependencies
FROM docker.io/maven:3-eclipse-temurin-${JDK_VERSION} AS build
ARG TARGET_DIR=dspace-installer
WORKDIR /app
# Create the 'dspace' user account & home directory
@@ -19,16 +19,64 @@ RUN chown -Rv dspace: /app
# Switch to dspace user & run below commands as that user
USER dspace
# Copy the DSpace source code (from local machine) into the workdir (excluding .dockerignore contents)
ADD --chown=dspace . /app/
# This next part may look odd, but it speeds up the build of this image *significantly*.
# Copy ONLY the POMs to this image (from local machine). This will allow us to download all dependencies *without*
# performing any code compilation steps.
# Parent POM
ADD --chown=dspace pom.xml /app/
RUN mkdir -p /app/dspace
# 'dspace' module POM. Includes 'additions' ONLY, as it's the only submodule that is required to exist.
ADD --chown=dspace dspace/pom.xml /app/dspace/
RUN mkdir -p /app/dspace/modules/
ADD --chown=dspace dspace/modules/pom.xml /app/dspace/modules/
RUN mkdir -p /app/dspace/modules/additions
ADD --chown=dspace dspace/modules/additions/pom.xml /app/dspace/modules/additions/
# 'dspace-api' module POM
RUN mkdir -p /app/dspace-api
ADD --chown=dspace dspace-api/pom.xml /app/dspace-api/
# 'dspace-iiif' module POM
RUN mkdir -p /app/dspace-iiif
ADD --chown=dspace dspace-iiif/pom.xml /app/dspace-iiif/
# 'dspace-oai' module POM
RUN mkdir -p /app/dspace-oai
ADD --chown=dspace dspace-oai/pom.xml /app/dspace-oai/
# 'dspace-rdf' module POM
RUN mkdir -p /app/dspace-rdf
ADD --chown=dspace dspace-rdf/pom.xml /app/dspace-rdf/
# 'dspace-saml2' module POM
RUN mkdir -p /app/dspace-saml2
ADD --chown=dspace dspace-saml2/pom.xml /app/dspace-saml2/
# 'dspace-server-webapp' module POM
RUN mkdir -p /app/dspace-server-webapp
ADD --chown=dspace dspace-server-webapp/pom.xml /app/dspace-server-webapp/
# 'dspace-services' module POM
RUN mkdir -p /app/dspace-services
ADD --chown=dspace dspace-services/pom.xml /app/dspace-services/
# 'dspace-sword' module POM
RUN mkdir -p /app/dspace-sword
ADD --chown=dspace dspace-sword/pom.xml /app/dspace-sword/
# 'dspace-swordv2' module POM
RUN mkdir -p /app/dspace-swordv2
ADD --chown=dspace dspace-swordv2/pom.xml /app/dspace-swordv2/
# Trigger the installation of all maven dependencies (hide download progress messages)
# Maven flags here ensure that we skip final assembly, skip building test environment and skip all code verification checks.
# These flags speed up this installation as much as reasonably possible.
ENV MAVEN_FLAGS="-P-assembly -P-test-environment -Denforcer.skip=true -Dcheckstyle.skip=true -Dlicense.skip=true -Dxml.skip=true"
RUN mvn --no-transfer-progress install ${MAVEN_FLAGS}
# These flags speed up this installation and skip tasks we cannot perform as we don't have the full source code.
ENV MAVEN_FLAGS="-P-assembly -P-test-environment -Denforcer.skip=true -Dcheckstyle.skip=true -Dlicense.skip=true -Dxjc.skip=true -Dxml.skip=true"
RUN mvn --no-transfer-progress verify ${MAVEN_FLAGS}
# Clear the contents of the /app directory (including all maven builds), so no artifacts remain.
# Clear the contents of the /app directory (including all maven target folders), so no artifacts remain.
# This ensures when dspace:dspace is built, it will use the Maven local cache (~/.m2) for dependencies
USER root
RUN rm -rf /app/*

View File

@@ -8,10 +8,14 @@
# This Dockerfile uses JDK17 by default.
# To build with other versions, use "--build-arg JDK_VERSION=[value]"
ARG JDK_VERSION=17
# The Docker version tag to build from
ARG DSPACE_VERSION=latest
# The Docker registry to use for DSpace images. Defaults to "docker.io"
# NOTE: non-DSpace images are hardcoded to use "docker.io" and are not impacted by this build argument
ARG DOCKER_REGISTRY=docker.io
# Step 1 - Run Maven Build
FROM dspace/dspace-dependencies:${DSPACE_VERSION} AS build
FROM ${DOCKER_REGISTRY}/dspace/dspace-dependencies:${DSPACE_VERSION} AS build
ARG TARGET_DIR=dspace-installer
WORKDIR /app
# The dspace-installer directory will be written to /install
@@ -30,38 +34,41 @@ RUN mvn --no-transfer-progress package && \
RUN rm -rf /install/webapps/server/
# Step 2 - Run Ant Deploy
FROM eclipse-temurin:${JDK_VERSION} AS ant_build
FROM docker.io/eclipse-temurin:${JDK_VERSION} AS ant_build
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
# Create the initial install deployment using ANT
ENV ANT_VERSION 1.10.12
ENV ANT_HOME /tmp/ant-$ANT_VERSION
ENV PATH $ANT_HOME/bin:$PATH
# Need wget to install ant
RUN apt-get update \
&& apt-get install -y --no-install-recommends wget \
&& apt-get purge -y --auto-remove \
&& rm -rf /var/lib/apt/lists/*
ENV ANT_VERSION=1.10.12
ENV ANT_HOME=/tmp/ant-$ANT_VERSION
ENV PATH=$ANT_HOME/bin:$PATH
# Download and install 'ant'
RUN mkdir $ANT_HOME && \
wget -qO- "https://archive.apache.org/dist/ant/binaries/apache-ant-$ANT_VERSION-bin.tar.gz" | tar -zx --strip-components=1 -C $ANT_HOME
curl --silent --show-error --location --fail --retry 5 --output /tmp/apache-ant.tar.gz \
https://archive.apache.org/dist/ant/binaries/apache-ant-${ANT_VERSION}-bin.tar.gz && \
tar -zx --strip-components=1 -f /tmp/apache-ant.tar.gz -C $ANT_HOME && \
rm /tmp/apache-ant.tar.gz
# Run necessary 'ant' deploy scripts
RUN ant init_installation update_configs update_code update_webapps
# Step 3 - Start up DSpace via Runnable JAR
FROM eclipse-temurin:${JDK_VERSION}
FROM docker.io/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
WORKDIR $DSPACE_INSTALL
# Need host command for "[dspace]/bin/make-handle-config"
RUN apt-get update \
&& apt-get install -y --no-install-recommends host \
&& apt-get purge -y --auto-remove \
&& rm -rf /var/lib/apt/lists/*
# 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
# enable JVM debugging via JDWP
ENV JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000
# On startup, run DSpace Runnable JAR
ENTRYPOINT ["java", "-jar", "webapps/server-boot.jar", "--dspace.dir=$DSPACE_INSTALL"]

View File

@@ -92,7 +92,7 @@ For more information on CheckStyle configurations below, see: http://checkstyle.
<!-- Requirements for Javadocs for methods -->
<module name="JavadocMethod">
<!-- All public methods MUST HAVE Javadocs -->
<property name="scope" value="public"/>
<property name="accessModifiers" value="public"/>
<!-- Allow params, throws and return tags to be optional -->
<property name="allowMissingParamTags" value="true"/>
<property name="allowMissingReturnTag" value="true"/>

View File

@@ -6,7 +6,7 @@ networks:
external: true
services:
dspace-cli:
image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-latest}"
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-latest}"
container_name: dspace-cli
build:
context: .

View File

@@ -28,7 +28,7 @@ services:
# from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above.
proxies__P__trusted__P__ipranges: '172.23.0'
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
build:
context: .
dockerfile: Dockerfile.test
@@ -64,7 +64,7 @@ services:
dspacedb:
container_name: dspacedb
# Uses a custom Postgres image with pgcrypto installed
image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}"
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}"
build:
# Must build out of subdirectory to have access to install script for pgcrypto
context: ./dspace/src/main/docker/dspace-postgres-pgcrypto/
@@ -84,7 +84,7 @@ services:
# DSpace Solr container
dspacesolr:
container_name: dspacesolr
image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}"
image: "${DOCKER_REGISTRY:-docker.io}/${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

View File

@@ -102,7 +102,7 @@
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.4.0</version>
<version>3.6.0</version>
<executions>
<execution>
<phase>validate</phase>
@@ -116,7 +116,7 @@
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>buildnumber-maven-plugin</artifactId>
<version>3.2.0</version>
<version>3.2.1</version>
<configuration>
<revisionOnScmFailure>UNKNOWN_REVISION</revisionOnScmFailure>
</configuration>
@@ -177,7 +177,7 @@
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxb2-maven-plugin</artifactId>
<version>3.1.0</version>
<version>3.2.0</version>
<executions>
<execution>
<id>workflow-curation</id>
@@ -341,6 +341,14 @@
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
@@ -388,6 +396,13 @@
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<exclusions>
<!-- Spring JCL is unnecessary and conflicts with commons-logging when both are on classpath -->
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-jcl</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
@@ -406,6 +421,16 @@
<groupId>org.mortbay.jasper</groupId>
<artifactId>apache-jsp</artifactId>
</exclusion>
<!-- Excluded BouncyCastle dependencies because we use a later version of BouncyCastle.
Having two versions of BouncyCastle in the classpath can cause Handle Server to throw errors. -->
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
</exclusion>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</exclusion>
</exclusions>
</dependency>
@@ -623,7 +648,7 @@
<dependency>
<groupId>dnsjava</groupId>
<artifactId>dnsjava</artifactId>
<version>3.6.0</version>
<version>3.6.3</version>
</dependency>
<dependency>
@@ -667,28 +692,6 @@
<version>${flyway.version}</version>
</dependency>
<!-- Google Analytics -->
<dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-analytics</artifactId>
</dependency>
<dependency>
<groupId>com.google.api-client</groupId>
<artifactId>google-api-client</artifactId>
</dependency>
<dependency>
<groupId>com.google.http-client</groupId>
<artifactId>google-http-client</artifactId>
</dependency>
<dependency>
<groupId>com.google.http-client</groupId>
<artifactId>google-http-client-jackson2</artifactId>
</dependency>
<dependency>
<groupId>com.google.oauth-client</groupId>
<artifactId>google-oauth-client</artifactId>
</dependency>
<!-- FindBugs -->
<dependency>
<groupId>com.google.code.findbugs</groupId>
@@ -702,7 +705,6 @@
<dependency>
<groupId>jakarta.inject</groupId>
<artifactId>jakarta.inject-api</artifactId>
<version>2.0.1</version>
</dependency>
<!-- JAXB API and implementation (no longer bundled as of Java 11) -->
@@ -733,7 +735,7 @@
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.12.261</version>
<version>1.12.781</version>
</dependency>
<!-- TODO: This may need to be replaced with the "orcid-model" artifact once this ticket is resolved:
@@ -748,6 +750,11 @@
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
</exclusion>
<!-- Exclude snakeyaml as a newer version is brought in by Spring Boot -->
<exclusion>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</exclusion>
</exclusions>
</dependency>
@@ -769,25 +776,27 @@
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>5.9</version>
<version>5.10</version>
</dependency>
<!-- Email templating -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.4.1</version>
</dependency>
<dependency>
<groupId>org.xmlunit</groupId>
<artifactId>xmlunit-core</artifactId>
<version>2.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.bcel</groupId>
<artifactId>bcel</artifactId>
<version>6.7.0</version>
<version>6.10.0</version>
<scope>test</scope>
</dependency>
@@ -814,7 +823,7 @@
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-junit-rule</artifactId>
<version>5.11.2</version>
<version>5.15.0</version>
<scope>test</scope>
<exclusions>
<!-- Exclude snakeyaml to avoid conflicts with: spring-boot-starter-cache -->
@@ -856,75 +865,4 @@
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- for mockserver -->
<!-- Solve dependency convergence issues related to Solr and
'mockserver-junit-rule' by selecting the versions we want to use. -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-buffer</artifactId>
<version>4.1.106.Final</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport</artifactId>
<version>4.1.106.Final</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-unix-common</artifactId>
<version>4.1.106.Final</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-common</artifactId>
<version>4.1.106.Final</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-handler</artifactId>
<version>4.1.106.Final</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec</artifactId>
<version>4.1.106.Final</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<dependency>
<groupId>org.xmlunit</groupId>
<artifactId>xmlunit-core</artifactId>
<version>2.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-schema-validator</artifactId>
<version>2.2.14</version>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-core</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>2.13.11</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>

View File

@@ -8,6 +8,7 @@
package org.dspace.access.status;
import java.sql.SQLException;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Objects;
@@ -26,7 +27,6 @@ import org.dspace.content.service.ItemService;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.eperson.Group;
import org.joda.time.LocalDate;
/**
* Default plugin implementation of the access status helper.
@@ -230,7 +230,7 @@ public class DefaultAccessStatusHelper implements AccessStatusHelper {
// If the policy is not valid there is an active embargo
Date startDate = policy.getStartDate();
if (startDate != null && !startDate.before(LocalDate.now().toDate())) {
if (startDate != null && !startDate.before(Date.from(Instant.now()))) {
// There is an active embargo: aim to take the shortest embargo (account for rare cases where
// more than one resource policy exists)
if (embargoDate == null) {

View File

@@ -18,6 +18,7 @@ import java.nio.charset.StandardCharsets;
import org.apache.commons.lang.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.poi.util.IOUtils;
import org.apache.tika.Tika;
import org.apache.tika.exception.TikaException;
import org.apache.tika.metadata.Metadata;
@@ -72,21 +73,23 @@ public class TikaTextExtractionFilter
// Not using temporary file. We'll use Tika's default in-memory parsing.
// Get maximum characters to extract. Default is 100,000 chars, which is also Tika's default setting.
String extractedText;
int maxChars = configurationService.getIntProperty("textextractor.max-chars", 100000);
int maxChars = configurationService.getIntProperty("textextractor.max-chars", 100_000);
try {
// Use Tika to extract text from input. Tika will automatically detect the file type.
Tika tika = new Tika();
tika.setMaxStringLength(maxChars); // Tell Tika the maximum number of characters to extract
IOUtils.setByteArrayMaxOverride(
configurationService.getIntProperty("textextractor.max-array", 100_000_000));
extractedText = tika.parseToString(source);
} catch (IOException e) {
System.err.format("Unable to extract text from bitstream in Item %s%n", currentItem.getID().toString());
e.printStackTrace();
e.printStackTrace(System.err);
log.error("Unable to extract text from bitstream in Item {}", currentItem.getID().toString(), e);
throw e;
} catch (OutOfMemoryError oe) {
System.err.format("OutOfMemoryError occurred when extracting text from bitstream in Item %s. " +
"You may wish to enable 'textextractor.use-temp-file'.%n", currentItem.getID().toString());
oe.printStackTrace();
oe.printStackTrace(System.err);
log.error("OutOfMemoryError occurred when extracting text from bitstream in Item {}. " +
"You may wish to enable 'textextractor.use-temp-file'.", currentItem.getID().toString(), oe);
throw oe;

View File

@@ -281,10 +281,14 @@ public class LogAnalyser {
*/
private static String fileTemplate = "dspace\\.log.*";
private static final ConfigurationService configurationService =
DSpaceServicesFactory.getInstance().getConfigurationService();
/**
* the configuration file from which to configure the analyser
*/
private static String configFile;
private static String configFile = configurationService.getProperty("dspace.dir")
+ File.separator + "config" + File.separator + "dstat.cfg";
/**
* the output file to which to write aggregation data
@@ -616,8 +620,6 @@ public class LogAnalyser {
}
// now do the host name and url lookup
ConfigurationService configurationService
= DSpaceServicesFactory.getInstance().getConfigurationService();
hostName = Utils.getHostName(configurationService.getProperty("dspace.ui.url"));
name = configurationService.getProperty("dspace.name").trim();
url = configurationService.getProperty("dspace.ui.url").trim();
@@ -658,8 +660,6 @@ public class LogAnalyser {
String myConfigFile, String myOutFile,
Date myStartDate, Date myEndDate,
boolean myLookUp) {
ConfigurationService configurationService
= DSpaceServicesFactory.getInstance().getConfigurationService();
if (myLogDir != null) {
logDir = myLogDir;
@@ -673,9 +673,6 @@ public class LogAnalyser {
if (myConfigFile != null) {
configFile = myConfigFile;
} else {
configFile = configurationService.getProperty("dspace.dir")
+ File.separator + "config" + File.separator + "dstat.cfg";
}
if (myStartDate != null) {

View File

@@ -46,8 +46,6 @@ Several "stock" implementations are provided.
<dd>writes event records to the Java logger.</dd>
<dt>{@link org.dspace.statistics.SolrLoggerUsageEventListener SolrLoggerUsageEventListener}</dt>
<dd>writes event records to Solr.</dd>
<dt>{@link org.dspace.google.GoogleRecorderEventListener GoogleRecorderEventListener}<.dt>
<dd>writes event records to Google Analytics.</dd>
</dl>
</body>
</html>

View File

@@ -523,9 +523,9 @@ public class AuthorizeUtil {
for (Collection coll : colls) {
if (!AuthorizeConfiguration
.canCollectionAdminPerformItemReinstatiate()) {
.canCollectionAdminPerformItemReinstate()) {
if (AuthorizeConfiguration
.canCommunityAdminPerformItemReinstatiate()
.canCommunityAdminPerformItemReinstate()
&& authorizeService.authorizeActionBoolean(context,
coll.getCommunities().get(0), Constants.ADMIN)) {
// authorized

View File

@@ -163,7 +163,7 @@ public class DCInput {
* The scope of the input sets, this restricts hidden metadata fields from
* view by the end user during submission.
*/
public static final String SUBMISSION_SCOPE = "submit";
public static final String SUBMISSION_SCOPE = "submission";
/**
* Class constructor for creating a DCInput object based on the contents of
@@ -262,7 +262,7 @@ public class DCInput {
/**
* Is this DCInput for display in the given scope? The scope should be
* either "workflow" or "submit", as per the input forms definition. If the
* either "workflow" or "submission", as per the input forms definition. If the
* internal visibility is set to "null" then this will always return true.
*
* @param scope String identifying the scope that this input's visibility

View File

@@ -14,7 +14,6 @@ import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.rometools.modules.opensearch.OpenSearchModule;
import com.rometools.modules.opensearch.entity.OSQuery;
@@ -58,12 +57,12 @@ public class OpenSearchServiceImpl implements OpenSearchService {
private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(OpenSearchServiceImpl.class);
// Namespaces used
protected final String osNs = "http://a9.com/-/spec/opensearch/1.1/";
protected final static String osNs = "http://a9.com/-/spec/opensearch/1.1/";
@Autowired(required = true)
@Autowired
protected ConfigurationService configurationService;
@Autowired(required = true)
@Autowired
protected HandleService handleService;
protected OpenSearchServiceImpl() {
@@ -119,11 +118,10 @@ public class OpenSearchServiceImpl implements OpenSearchService {
@Override
public String getResultsString(Context context, String format, String query, int totalResults, int start,
int pageSize,
IndexableObject scope, List<IndexableObject> results,
Map<String, String> labels) throws IOException {
int pageSize, IndexableObject scope, List<IndexableObject> results)
throws IOException {
try {
return getResults(context, format, query, totalResults, start, pageSize, scope, results, labels)
return getResults(context, format, query, totalResults, start, pageSize, scope, results)
.outputString();
} catch (FeedException e) {
log.error(e.toString(), e);
@@ -133,11 +131,10 @@ public class OpenSearchServiceImpl implements OpenSearchService {
@Override
public Document getResultsDoc(Context context, String format, String query, int totalResults, int start,
int pageSize,
IndexableObject scope, List<IndexableObject> results, Map<String, String> labels)
int pageSize, IndexableObject scope, List<IndexableObject> results)
throws IOException {
try {
return getResults(context, format, query, totalResults, start, pageSize, scope, results, labels)
return getResults(context, format, query, totalResults, start, pageSize, scope, results)
.outputW3CDom();
} catch (FeedException e) {
log.error(e.toString(), e);
@@ -146,8 +143,7 @@ public class OpenSearchServiceImpl implements OpenSearchService {
}
protected SyndicationFeed getResults(Context context, String format, String query, int totalResults, int start,
int pageSize, IndexableObject scope,
List<IndexableObject> results, Map<String, String> labels) {
int pageSize, IndexableObject scope, List<IndexableObject> results) {
// Encode results in requested format
if ("rss".equals(format)) {
format = "rss_2.0";
@@ -156,7 +152,7 @@ public class OpenSearchServiceImpl implements OpenSearchService {
}
SyndicationFeed feed = new SyndicationFeed();
feed.populate(null, context, scope, results, labels);
feed.populate(null, context, scope, results);
feed.setType(format);
feed.addModule(openSearchMarkup(query, totalResults, start, pageSize));
return feed;

View File

@@ -11,6 +11,7 @@ import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -135,8 +136,6 @@ public class SyndicationFeed {
protected String[] podcastableMIMETypes =
configurationService.getArrayProperty("webui.feed.podcast.mimetypes", new String[] {"audio/x-mpeg"});
// -------- Instance variables:
// the feed object we are building
protected SyndFeed feed = null;
@@ -146,9 +145,6 @@ public class SyndicationFeed {
protected CommunityService communityService;
protected ItemService itemService;
/**
* Constructor.
*/
public SyndicationFeed() {
feed = new SyndFeedImpl();
ContentServiceFactory contentServiceFactory = ContentServiceFactory.getInstance();
@@ -157,16 +153,6 @@ public class SyndicationFeed {
communityService = contentServiceFactory.getCommunityService();
}
/**
* Returns list of metadata selectors used to compose the description element
*
* @return selector list - format 'schema.element[.qualifier]'
*/
public static String[] getDescriptionSelectors() {
return (String[]) ArrayUtils.clone(descriptionFields);
}
/**
* Fills in the feed and entry-level metadata from DSpace objects.
*
@@ -174,15 +160,17 @@ public class SyndicationFeed {
* @param context context
* @param dso the scope
* @param items array of objects
* @param labels label map
*/
public void populate(HttpServletRequest request, Context context, IndexableObject dso,
List<IndexableObject> items, Map<String, String> labels) {
List<IndexableObject> items) {
String logoURL = null;
String objectURL = null;
String defaultTitle = null;
boolean podcastFeed = false;
this.request = request;
Map<String, String> labels = getLabels();
// dso is null for the whole site, or a search without scope
if (dso == null) {
defaultTitle = configurationService.getProperty("dspace.name");
@@ -553,5 +541,19 @@ public class SyndicationFeed {
List<MetadataValue> dcv = itemService.getMetadataByMetadataString(item, field);
return (dcv.size() > 0) ? dcv.get(0).getValue() : null;
}
}
/**
* Internal method to get labels for the returned document
*/
private Map<String, String> getLabels() {
// TODO: get strings from translation file or configuration
Map<String, String> labelMap = new HashMap<>();
labelMap.put(SyndicationFeed.MSG_UNTITLED, "notitle");
labelMap.put(SyndicationFeed.MSG_LOGO_TITLE, "logo.title");
labelMap.put(SyndicationFeed.MSG_FEED_DESCRIPTION, "general-feed.description");
for (String selector : descriptionFields) {
labelMap.put("metadata." + selector, selector);
}
return labelMap;
}
}

View File

@@ -10,7 +10,6 @@ package org.dspace.app.util.service;
import java.io.IOException;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import org.dspace.content.DSpaceObject;
import org.dspace.core.Context;
@@ -86,14 +85,12 @@ public interface OpenSearchService {
* @param pageSize - page size
* @param scope - search scope, null or the community/collection
* @param results the retrieved DSpace objects satisfying search
* @param labels labels to apply - format specific
* @return formatted search results
* @throws IOException if IO error
*/
public String getResultsString(Context context, String format, String query, int totalResults, int start,
int pageSize,
IndexableObject scope, List<IndexableObject> results,
Map<String, String> labels) throws IOException;
int pageSize, IndexableObject scope, List<IndexableObject> results)
throws IOException;
/**
* Returns a formatted set of search results as a document
@@ -106,13 +103,11 @@ public interface OpenSearchService {
* @param pageSize - page size
* @param scope - search scope, null or the community/collection
* @param results the retrieved DSpace objects satisfying search
* @param labels labels to apply - format specific
* @return formatted search results
* @throws IOException if IO error
*/
public Document getResultsDoc(Context context, String format, String query, int totalResults, int start,
int pageSize,
IndexableObject scope, List<IndexableObject> results, Map<String, String> labels)
int pageSize, IndexableObject scope, List<IndexableObject> results)
throws IOException;
public DSpaceObject resolveScope(Context context, String scope) throws SQLException;

View File

@@ -17,6 +17,7 @@ import java.util.Collections;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
@@ -68,12 +69,8 @@ import org.dspace.services.factory.DSpaceServicesFactory;
* @author Ivan Masár
* @author Michael Plate
*/
public class LDAPAuthentication
implements AuthenticationMethod {
public class LDAPAuthentication implements AuthenticationMethod {
/**
* log4j category
*/
private static final Logger log
= org.apache.logging.log4j.LogManager.getLogger(LDAPAuthentication.class);
@@ -130,7 +127,7 @@ public class LDAPAuthentication
return false;
}
/*
/**
* This is an explicit method.
*/
@Override
@@ -138,7 +135,7 @@ public class LDAPAuthentication
return false;
}
/*
/**
* Add authenticated users to the group defined in dspace.cfg by
* the login.specialgroup key.
*/
@@ -177,7 +174,7 @@ public class LDAPAuthentication
return Collections.EMPTY_LIST;
}
/*
/**
* Authenticate the given credentials.
* This is the heart of the authentication method: test the
* credentials for authenticity, and if accepted, attempt to match
@@ -187,7 +184,7 @@ public class LDAPAuthentication
* @param context
* DSpace context, will be modified (ePerson set) upon success.
*
* @param username
* @param netid
* Username (or email address) when method is explicit. Use null for
* implicit method.
*
@@ -250,7 +247,7 @@ public class LDAPAuthentication
}
// Check a DN was found
if ((dn == null) || (dn.trim().equals(""))) {
if (StringUtils.isBlank(dn)) {
log.info(LogHelper
.getHeader(context, "failed_login", "no DN found for user " + netid));
return BAD_CREDENTIALS;
@@ -269,6 +266,18 @@ public class LDAPAuthentication
context.setCurrentUser(eperson);
request.setAttribute(LDAP_AUTHENTICATED, true);
// update eperson's attributes
context.turnOffAuthorisationSystem();
setEpersonAttributes(context, eperson, ldap, Optional.empty());
try {
ePersonService.update(context, eperson);
context.dispatchEvents();
} catch (AuthorizeException e) {
log.warn("update of eperson " + eperson.getID() + " failed", e);
} finally {
context.restoreAuthSystemState();
}
// assign user to groups based on ldap dn
assignGroups(dn, ldap.ldapGroup, context);
@@ -313,14 +322,13 @@ public class LDAPAuthentication
log.info(LogHelper.getHeader(context,
"type=ldap-login", "type=ldap_but_already_email"));
context.turnOffAuthorisationSystem();
eperson.setNetid(netid.toLowerCase());
setEpersonAttributes(context, eperson, ldap, Optional.of(netid));
ePersonService.update(context, eperson);
context.dispatchEvents();
context.restoreAuthSystemState();
context.setCurrentUser(eperson);
request.setAttribute(LDAP_AUTHENTICATED, true);
// assign user to groups based on ldap dn
assignGroups(dn, ldap.ldapGroup, context);
@@ -331,20 +339,7 @@ public class LDAPAuthentication
try {
context.turnOffAuthorisationSystem();
eperson = ePersonService.create(context);
if (StringUtils.isNotEmpty(email)) {
eperson.setEmail(email);
}
if (StringUtils.isNotEmpty(ldap.ldapGivenName)) {
eperson.setFirstName(context, ldap.ldapGivenName);
}
if (StringUtils.isNotEmpty(ldap.ldapSurname)) {
eperson.setLastName(context, ldap.ldapSurname);
}
if (StringUtils.isNotEmpty(ldap.ldapPhone)) {
ePersonService.setMetadataSingleValue(context, eperson,
MD_PHONE, ldap.ldapPhone, null);
}
eperson.setNetid(netid.toLowerCase());
setEpersonAttributes(context, eperson, ldap, Optional.of(netid));
eperson.setCanLogIn(true);
authenticationService.initEPerson(context, request, eperson);
ePersonService.update(context, eperson);
@@ -382,6 +377,29 @@ public class LDAPAuthentication
return BAD_ARGS;
}
/**
* Update eperson's attributes
*/
private void setEpersonAttributes(Context context, EPerson eperson, SpeakerToLDAP ldap, Optional<String> netid)
throws SQLException {
if (StringUtils.isNotEmpty(ldap.ldapEmail)) {
eperson.setEmail(ldap.ldapEmail);
}
if (StringUtils.isNotEmpty(ldap.ldapGivenName)) {
eperson.setFirstName(context, ldap.ldapGivenName);
}
if (StringUtils.isNotEmpty(ldap.ldapSurname)) {
eperson.setLastName(context, ldap.ldapSurname);
}
if (StringUtils.isNotEmpty(ldap.ldapPhone)) {
ePersonService.setMetadataSingleValue(context, eperson, MD_PHONE, ldap.ldapPhone, null);
}
if (netid.isPresent()) {
eperson.setNetid(netid.get().toLowerCase());
}
}
/**
* Internal class to manage LDAP query and results, mainly
* because there are multiple values to return.
@@ -503,6 +521,7 @@ public class LDAPAuthentication
} else {
searchName = ldap_provider_url + ldap_search_context;
}
@SuppressWarnings("BanJNDI")
NamingEnumeration<SearchResult> answer = ctx.search(
searchName,
"(&({0}={1}))", new Object[] {ldap_id_field,
@@ -553,7 +572,7 @@ public class LDAPAuthentication
att = atts.get(attlist[4]);
if (att != null) {
// loop through all groups returned by LDAP
ldapGroup = new ArrayList<String>();
ldapGroup = new ArrayList<>();
for (NamingEnumeration val = att.getAll(); val.hasMoreElements(); ) {
ldapGroup.add((String) val.next());
}
@@ -633,7 +652,8 @@ public class LDAPAuthentication
ctx.addToEnvironment(javax.naming.Context.AUTHORITATIVE, "true");
ctx.addToEnvironment(javax.naming.Context.REFERRAL, "follow");
// dummy operation to check if authentication has succeeded
ctx.getAttributes("");
@SuppressWarnings("BanJNDI")
Attributes trash = ctx.getAttributes("");
} else if (!useTLS) {
// Authenticate
env.put(javax.naming.Context.SECURITY_AUTHENTICATION, "Simple");
@@ -671,7 +691,7 @@ public class LDAPAuthentication
}
}
/*
/**
* Returns the URL of an external login page which is not applicable for this authn method.
*
* Note: Prior to DSpace 7, this method return the page of login servlet.
@@ -699,7 +719,7 @@ public class LDAPAuthentication
return "ldap";
}
/*
/**
* Add authenticated users to the group defined in dspace.cfg by
* the authentication-ldap.login.groupmap.* key.
*

View File

@@ -0,0 +1,711 @@
/**
* 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.authenticate;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.authenticate.factory.AuthenticateServiceFactory;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.MetadataField;
import org.dspace.content.MetadataSchema;
import org.dspace.content.MetadataSchemaEnum;
import org.dspace.content.NonUniqueMetadataException;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.MetadataFieldService;
import org.dspace.content.service.MetadataSchemaService;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.eperson.factory.EPersonServiceFactory;
import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.GroupService;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
/**
* SAML authentication for DSpace.
*
* @author Ray Lee
*/
public class SamlAuthentication implements AuthenticationMethod {
private static final Logger log = LogManager.getLogger(SamlAuthentication.class);
// Additional metadata mappings.
protected Map<String, String> metadataHeaderMap = null;
protected EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
protected GroupService groupService = EPersonServiceFactory.getInstance().getGroupService();
protected MetadataFieldService metadataFieldService = ContentServiceFactory.getInstance().getMetadataFieldService();
protected MetadataSchemaService metadataSchemaService =
ContentServiceFactory.getInstance().getMetadataSchemaService();
protected ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
/**
* Authenticate the given or implicit credentials. This is the heart of the
* authentication method: test the credentials for authenticity, and if
* accepted, attempt to match (or optionally, create) an
* <code>EPerson</code>. If an <code>EPerson</code> is found it is set in
* the <code>Context</code> that was passed.
*
* DSpace supports authentication using NetID or email address. A user's NetID
* is a unique identifier from the IdP that identifies a particular user. The
* NetID can be of almost any form, such as a unique integer or string. In
* SAML, this is referred to as a Name ID.
*
* There are two ways to supply identity information to DSpace:
*
* 1) Name ID from SAML attribute (best)
*
* The Name ID-based method is superior because users may change their email
* address with the identity provider. When this happens DSpace will not be
* able to associate their new address with their old account.
*
* 2) Email address from SAML attribute (okay)
*
* In the case where a Name ID header is not available or not found DSpace
* will fall back to identifying a user based upon their email address.
*
* Identity Scheme Migration Strategies:
*
* If you are currently using Email based authentication (either 1 or 2) and
* want to upgrade to NetID based authentication then there is an easy path.
* Coordinate with the IdP to provide a Name ID in the SAML assertion. When a
* user attempts to log in, DSpace will first look for an EPerson with the
* passed Name ID. When this fails, DSpace will fall back to email based
* authentication. Then DSpace will update the user's EPerson account record
* to set their NetID, so all future authentications for this user will be based
* upon NetID.
*
* DSpace will prevent an account from switching NetIDs. If an account already
* has a NetID set, and a user tries to authenticate with the same email but
* a different NetID, the authentication will fail.
*
* @param context DSpace context, will be modified (EPerson set) upon success.
* @param username Not used by SAML-based authentication.
* @param password Not used by SAML-based authentication.
* @param realm Not used by SAML-based authentication.
* @param request The HTTP request that started this operation.
* @return one of: SUCCESS, NO_SUCH_USER, BAD_ARGS
* @throws SQLException if a database error occurs.
*/
@Override
public int authenticate(Context context, String username, String password,
String realm, HttpServletRequest request) throws SQLException {
if (request == null) {
log.warn("Unable to authenticate using SAML because the request object is null.");
return BAD_ARGS;
}
// Initialize additional EPerson metadata mappings.
initialize(context);
String nameId = findSingleAttribute(request, getNameIdAttributeName());
if (log.isDebugEnabled()) {
log.debug("Starting SAML Authentication");
log.debug("Received name ID: " + nameId);
}
// Should we auto register new users?
boolean autoRegister = configurationService.getBooleanProperty("authentication-saml.autoregister", true);
// Four steps to authenticate a user:
try {
// Step 1: Identify user
EPerson eperson = findEPerson(context, request);
// Step 2: Register new user, if necessary
if (eperson == null && autoRegister) {
eperson = registerNewEPerson(context, request);
}
if (eperson == null) {
return AuthenticationMethod.NO_SUCH_USER;
}
if (!eperson.canLogIn()) {
return AuthenticationMethod.BAD_ARGS;
}
// Step 3: Update user's metadata
updateEPerson(context, request, eperson);
// Step 4: Log the user in
context.setCurrentUser(eperson);
request.setAttribute("saml.authenticated", true);
AuthenticateServiceFactory.getInstance().getAuthenticationService().initEPerson(context, request, eperson);
log.info(eperson.getEmail() + " has been authenticated via SAML.");
return AuthenticationMethod.SUCCESS;
} catch (Throwable t) {
// Log the error, and undo the authentication before returning a failure.
log.error("Unable to successfully authenticate using SAML for user because of an exception.", t);
context.setCurrentUser(null);
return AuthenticationMethod.NO_SUCH_USER;
}
}
@Override
public List<Group> getSpecialGroups(Context context, HttpServletRequest request) throws SQLException {
return List.of();
}
@Override
public boolean allowSetPassword(Context context, HttpServletRequest request, String email) throws SQLException {
// SAML authentication doesn't use a password.
return false;
}
@Override
public boolean isImplicit() {
return false;
}
@Override
public boolean canSelfRegister(Context context, HttpServletRequest request,
String username) throws SQLException {
// SAML will auto create accounts if configured to do so, but that is not
// the same as self register. Self register means that the user can sign up for
// an account from the web. This is not supported with SAML.
return false;
}
@Override
public void initEPerson(Context context, HttpServletRequest request,
EPerson eperson) throws SQLException {
// We don't do anything because all our work is done in authenticate.
}
/**
* Returns the URL in the SAML relying party service that initiates a login with the IdP,
* as configured.
*
* @see AuthenticationMethod#loginPageURL(Context, HttpServletRequest, HttpServletResponse)
*/
@Override
public String loginPageURL(Context context, HttpServletRequest request, HttpServletResponse response) {
String samlLoginUrl = configurationService.getProperty("authentication-saml.authenticate-endpoint");
return response.encodeRedirectURL(samlLoginUrl);
}
@Override
public String getName() {
return "saml";
}
/**
* Check if the SAML plugin is enabled.
*
* @return true if enabled, false otherwise
*/
public static boolean isEnabled() {
final String samlPluginName = new SamlAuthentication().getName();
boolean samlEnabled = false;
// Loop through all enabled authentication plugins to see if SAML is one of them.
Iterator<AuthenticationMethod> authenticationMethodIterator =
AuthenticateServiceFactory.getInstance().getAuthenticationService().authenticationMethodIterator();
while (authenticationMethodIterator.hasNext()) {
if (samlPluginName.equals(authenticationMethodIterator.next().getName())) {
samlEnabled = true;
break;
}
}
return samlEnabled;
}
/**
* Identify an existing EPerson based upon the SAML attributes provided on
* the request object.
*
* 1) Name ID from SAML attribute (best)
* The Name ID-based method is superior because users may change their email
* address with the identity provider. When this happens DSpace will not be
* able to associate their new address with their old account.
*
* 2) Email address from SAML attribute (okay)
* In the case where a Name ID header is not available or not found DSpace
* will fall back to identifying a user based upon their email address.
*
* If successful then the identified EPerson will be returned, otherwise null.
*
* @param context The DSpace database context
* @param request The current HTTP Request
* @return The EPerson identified or null.
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
protected EPerson findEPerson(Context context, HttpServletRequest request) throws SQLException, AuthorizeException {
String nameId = findSingleAttribute(request, getNameIdAttributeName());
if (nameId != null) {
EPerson ePerson = ePersonService.findByNetid(context, nameId);
if (ePerson == null) {
log.info("Unable to identify EPerson by netid (SAML name ID): " + nameId);
} else {
log.info("Identified EPerson by netid (SAML name ID): " + nameId);
return ePerson;
}
}
String emailAttributeName = getEmailAttributeName();
String email = findSingleAttribute(request, emailAttributeName);
if (email != null) {
email = email.toLowerCase();
EPerson ePerson = ePersonService.findByEmail(context, email);
if (ePerson == null) {
log.info("Unable to identify EPerson by email: " + emailAttributeName + "=" + email);
} else {
log.info("Identified EPerson by email: " + emailAttributeName + "=" + email);
if (ePerson.getNetid() == null) {
return ePerson;
}
// The user has a netid that differs from the received SAML name ID.
log.error("SAML authentication identified EPerson by email: " + emailAttributeName + "=" + email);
log.error("Received SAML name ID: " + nameId);
log.error("EPerson has netid: " + ePerson.getNetid());
log.error(
"The SAML name ID is expected to be the same as the EPerson netid. " +
"This might be a hacking attempt to steal another user's credentials. If the " +
"user's netid has changed you will need to manually change it to the correct " +
"value or unset it in the database.");
}
}
if (nameId == null && email == null) {
log.error(
"SAML authentication did not find a name ID or email in the request from which to indentify a user");
}
return null;
}
/**
* Register a new EPerson. This method is called when no existing user was
* found for the NetID or email and autoregister is enabled. When these conditions
* are met this method will create a new EPerson object.
*
* In order to create a new EPerson object there is a minimal set of metadata
* required: email, first name, and last name. If we don't have access to these
* three pieces of information then we will be unable to create a new EPerson.
*
* Note that this method only adds the minimal metadata. Any additional metadata
* will need to be added by the updateEPerson method.
*
* @param context The current DSpace database context
* @param request The current HTTP Request
* @return A new EPerson object or null if unable to create a new EPerson.
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
protected EPerson registerNewEPerson(Context context, HttpServletRequest request)
throws SQLException, AuthorizeException {
String nameId = findSingleAttribute(request, getNameIdAttributeName());
String emailAttributeName = getEmailAttributeName();
String firstNameAttributeName = getFirstNameAttributeName();
String lastNameAttributeName = getLastNameAttributeName();
String email = findSingleAttribute(request, emailAttributeName);
String firstName = findSingleAttribute(request, firstNameAttributeName);
String lastName = findSingleAttribute(request, lastNameAttributeName);
if (email == null || firstName == null || lastName == null) {
// We require that there be an email, first name, and last name.
String message = "Unable to register new eperson because we are unable to find an email address, " +
"first name, and last name for the user.\n";
message += " name ID: " + nameId + "\n";
message += " email: " + emailAttributeName + "=" + email + "\n";
message += " first name: " + firstNameAttributeName + "=" + firstName + "\n";
message += " last name: " + lastNameAttributeName + "=" + lastName;
log.error(message);
return null;
}
try {
context.turnOffAuthorisationSystem();
EPerson ePerson = ePersonService.create(context);
// Set the minimum attributes for the new eperson
if (nameId != null) {
ePerson.setNetid(nameId);
}
ePerson.setEmail(email.toLowerCase());
ePerson.setFirstName(context, firstName);
ePerson.setLastName(context, lastName);
ePerson.setCanLogIn(true);
ePerson.setSelfRegistered(true);
// Commit the new eperson
AuthenticateServiceFactory.getInstance().getAuthenticationService().initEPerson(context, request, ePerson);
ePersonService.update(context, ePerson);
context.dispatchEvents();
if (log.isInfoEnabled()) {
String message = "Auto registered new eperson using SAML attributes:\n";
message += " netid: " + ePerson.getNetid() + "\n";
message += " email: " + ePerson.getEmail() + "\n";
message += " firstName: " + ePerson.getFirstName() + "\n";
message += " lastName: " + ePerson.getLastName();
log.info(message);
}
return ePerson;
} catch (SQLException | AuthorizeException e) {
log.error(e.getMessage(), e);
throw e;
} finally {
context.restoreAuthSystemState();
}
}
/**
* After we successfully authenticated a user, this method will update the user's attributes. The
* user's email, name, or other attribute may have been changed since the last time they
* logged into DSpace. This method will update the database with their most recent information.
*
* This method handles the basic DSpace metadata (email, first name, last name) along with
* additional metadata set using the setMetadata() methods on the EPerson object. The
* additional metadata mappings are defined in configuration.
*
* @param context The current DSpace database context
* @param request The current HTTP Request
* @param eperson The eperson object to update.
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
protected void updateEPerson(Context context, HttpServletRequest request, EPerson eperson)
throws SQLException, AuthorizeException {
String nameId = findSingleAttribute(request, getNameIdAttributeName());
String emailAttributeName = getEmailAttributeName();
String firstNameAttributeName = getFirstNameAttributeName();
String lastNameAttributeName = getLastNameAttributeName();
String email = findSingleAttribute(request, emailAttributeName);
String firstName = findSingleAttribute(request, firstNameAttributeName);
String lastName = findSingleAttribute(request, lastNameAttributeName);
try {
context.turnOffAuthorisationSystem();
// 1) Update the minimum metadata
// Only update the netid if none has been previously set. This can occur when a repo switches
// to netid based authentication. The current users do not have netids and fall back to email-based
// identification but once they login we update their record and lock the account to a particular netid.
if (nameId != null && eperson.getNetid() == null) {
eperson.setNetid(nameId);
}
// The email could have changed if using netid based lookup.
if (email != null) {
eperson.setEmail(email.toLowerCase());
}
if (firstName != null) {
eperson.setFirstName(context, firstName);
}
if (lastName != null) {
eperson.setLastName(context, lastName);
}
if (log.isDebugEnabled()) {
String message = "Updated the eperson's minimal metadata: \n";
message += " Email: " + emailAttributeName + "=" + email + "' \n";
message += " First name: " + firstNameAttributeName + "=" + firstName + "\n";
message += " Last name: " + lastNameAttributeName + "=" + lastName;
log.debug(message);
}
// 2) Update additional eperson metadata
for (String attributeName : metadataHeaderMap.keySet()) {
String metadataFieldName = metadataHeaderMap.get(attributeName);
String value = findSingleAttribute(request, attributeName);
// Truncate values
if (value == null) {
log.warn("Unable to update the eperson's '{}' metadata"
+ " because the attribute '{}' does not exist.", metadataFieldName, attributeName);
continue;
}
ePersonService.setMetadataSingleValue(context, eperson,
MetadataSchemaEnum.EPERSON.getName(), metadataFieldName, null, null, value);
log.debug("Updated the eperson's {} metadata using attribute: {}={}",
metadataFieldName, attributeName, value);
}
ePersonService.update(context, eperson);
context.dispatchEvents();
} catch (SQLException | AuthorizeException e) {
log.error(e.getMessage(), e);
throw e;
} finally {
context.restoreAuthSystemState();
}
}
/**
* Initialize SAML Authentication.
*
* During initalization the mapping of additional EPerson metadata will be loaded from the configuration
* and cached. While loading the metadata mapping this method will check the EPerson object to see
* if it supports the metadata field. If the field is not supported and autocreate is turned on then
* the field will be automatically created.
*
* It is safe to call this method multiple times.
*
* @param context context
* @throws SQLException if database error
*/
protected synchronized void initialize(Context context) throws SQLException {
if (metadataHeaderMap != null) {
return;
}
HashMap<String, String> map = new HashMap<>();
String[] mappingString = configurationService.getArrayProperty("authentication-saml.eperson.metadata");
boolean autoCreate = configurationService
.getBooleanProperty("authentication-saml.eperson.metadata.autocreate", false);
// Bail out if not set, returning an empty map.
if (mappingString == null || mappingString.length == 0) {
log.debug("No additional eperson metadata mapping found: authentication-saml.eperson.metadata");
metadataHeaderMap = map;
return;
}
log.debug("Loading additional eperson metadata from: authentication-saml.eperson.metadata="
+ StringUtils.join(mappingString, ","));
for (String metadataString : mappingString) {
metadataString = metadataString.trim();
String[] metadataParts = metadataString.split("=>");
if (metadataParts.length != 2) {
log.error("Unable to parse metadata mapping string: '" + metadataString + "'");
continue;
}
String attributeName = metadataParts[0].trim();
String metadataFieldName = metadataParts[1].trim().toLowerCase();
boolean valid = checkIfEPersonMetadataFieldExists(context, metadataFieldName);
if (!valid && autoCreate) {
valid = autoCreateEPersonMetadataField(context, metadataFieldName);
}
if (valid) {
// The eperson field is fine, we can use it.
log.debug("Loading additional eperson metadata mapping for: {}={}",
attributeName, metadataFieldName);
map.put(attributeName, metadataFieldName);
} else {
// The field doesn't exist, and we can't use it.
log.error("Skipping the additional eperson metadata mapping for: {}={}"
+ " because the field is not supported by the current configuration.",
attributeName, metadataFieldName);
}
}
metadataHeaderMap = map;
}
/**
* Check if a metadata field for an EPerson is available.
*
* @param metadataName The name of the metadata field.
* @param context context
* @return True if a valid metadata field, otherwise false.
* @throws SQLException if database error
*/
protected synchronized boolean checkIfEPersonMetadataFieldExists(Context context, String metadataName)
throws SQLException {
if (metadataName == null) {
return false;
}
MetadataField metadataField = metadataFieldService.findByElement(
context, MetadataSchemaEnum.EPERSON.getName(), metadataName, null);
return metadataField != null;
}
/**
* Validate metadata field names
*/
protected final String FIELD_NAME_REGEX = "^[_A-Za-z0-9]+$";
/**
* Automatically create a new metadata field for an EPerson
*
* @param context context
* @param metadataName The name of the new metadata field.
* @return True if successful, otherwise false.
* @throws SQLException if database error
*/
protected synchronized boolean autoCreateEPersonMetadataField(Context context, String metadataName)
throws SQLException {
if (metadataName == null) {
return false;
}
if (!metadataName.matches(FIELD_NAME_REGEX)) {
return false;
}
MetadataSchema epersonSchema = metadataSchemaService.find(context, "eperson");
MetadataField metadataField = null;
try {
context.turnOffAuthorisationSystem();
metadataField = metadataFieldService.create(context, epersonSchema, metadataName, null, null);
} catch (AuthorizeException | NonUniqueMetadataException e) {
log.error(e.getMessage(), e);
return false;
} finally {
context.restoreAuthSystemState();
}
return metadataField != null;
}
@Override
public boolean isUsed(final Context context, final HttpServletRequest request) {
if (request != null &&
context.getCurrentUser() != null &&
request.getAttribute("saml.authenticated") != null
) {
return true;
}
return false;
}
@Override
public boolean canChangePassword(Context context, EPerson ePerson, String currentPassword) {
return false;
}
private String findSingleAttribute(HttpServletRequest request, String name) {
if (StringUtils.isBlank(name)) {
return null;
}
Object value = request.getAttribute(name);
if (value instanceof List) {
List<?> list = (List<?>) value;
if (list.size() == 0) {
value = null;
} else {
value = list.get(0);
}
}
return (value == null ? null : value.toString());
}
private String getNameIdAttributeName() {
return configurationService.getProperty("authentication-saml.attribute.name-id", "org.dspace.saml.NAME_ID");
}
private String getEmailAttributeName() {
return configurationService.getProperty("authentication-saml.attribute.email", "org.dspace.saml.EMAIL");
}
private String getFirstNameAttributeName() {
return configurationService.getProperty("authentication-saml.attribute.first-name",
"org.dspace.saml.GIVEN_NAME");
}
private String getLastNameAttributeName() {
return configurationService.getProperty("authentication-saml.attribute.last-name", "org.dspace.saml.SURNAME");
}
}

View File

@@ -174,9 +174,9 @@ public class AuthorizeConfiguration {
*
* @return true/false
*/
public static boolean canCommunityAdminPerformItemReinstatiate() {
public static boolean canCommunityAdminPerformItemReinstate() {
init();
return configurationService.getBooleanProperty("core.authorization.community-admin.item.reinstatiate", true);
return configurationService.getBooleanProperty("core.authorization.community-admin.item.reinstate", true);
}
/**
@@ -306,9 +306,9 @@ public class AuthorizeConfiguration {
*
* @return true/false
*/
public static boolean canCollectionAdminPerformItemReinstatiate() {
public static boolean canCollectionAdminPerformItemReinstate() {
init();
return configurationService.getBooleanProperty("core.authorization.collection-admin.item.reinstatiate", true);
return configurationService.getBooleanProperty("core.authorization.collection-admin.item.reinstate", true);
}
/**

View File

@@ -422,9 +422,6 @@ public class BrowseEngine {
}
}
// this is the total number of results in answer to the query
int total = getTotalResults(true);
// set the ordering field (there is only one option)
dao.setOrderField("sort_value");
@@ -444,6 +441,9 @@ public class BrowseEngine {
dao.setOffset(offset);
dao.setLimit(scope.getResultsPerPage());
// this is the total number of results in answer to the query
int total = getTotalResults(true);
// Holder for the results
List<String[]> results = null;
@@ -680,33 +680,9 @@ public class BrowseEngine {
// tell the browse query whether we are distinct
dao.setDistinct(distinct);
// ensure that the select is set to "*"
String[] select = {"*"};
dao.setCountValues(select);
// FIXME: it would be nice to have a good way of doing this in the DAO
// now reset all of the fields that we don't want to have constraining
// our count, storing them locally to reinstate later
String focusField = dao.getJumpToField();
String focusValue = dao.getJumpToValue();
int limit = dao.getLimit();
int offset = dao.getOffset();
dao.setJumpToField(null);
dao.setJumpToValue(null);
dao.setLimit(-1);
dao.setOffset(-1);
// perform the query and get the result
int count = dao.doCountQuery();
// now put back the values we removed for this method
dao.setJumpToField(focusField);
dao.setJumpToValue(focusValue);
dao.setLimit(limit);
dao.setOffset(offset);
dao.setCountValues(null);
log.debug(LogHelper.getHeader(context, "get_total_results_return", "return=" + count));
return count;

View File

@@ -543,19 +543,6 @@ public class BrowseIndex {
return getTableName(false, false, true, false);
}
/**
* Get the name of the column that is used to store the default value column
*
* @return the name of the value column
*/
public String getValueColumn() {
if (!isDate()) {
return "sort_text_value";
} else {
return "text_value";
}
}
/**
* Get the name of the primary key index column
*
@@ -565,35 +552,6 @@ public class BrowseIndex {
return "id";
}
/**
* Is this browse index type for a title?
*
* @return true if title type, false if not
*/
// public boolean isTitle()
// {
// return "title".equals(getDataType());
// }
/**
* Is the browse index type for a date?
*
* @return true if date type, false if not
*/
public boolean isDate() {
return "date".equals(getDataType());
}
/**
* Is the browse index type for a plain text type?
*
* @return true if plain text type, false if not
*/
// public boolean isText()
// {
// return "text".equals(getDataType());
// }
/**
* Is the browse index of display type single?
*

View File

@@ -13,6 +13,8 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.apache.solr.client.solrj.util.ClientUtils;
@@ -180,18 +182,33 @@ public class SolrBrowseDAO implements BrowseDAO {
addDefaultFilterQueries(query);
if (distinct) {
DiscoverFacetField dff;
// To get the number of distinct values we use the next "json.facet" query param
// {"entries_count": {"type":"terms","field": "<fieldName>_filter", "limit":0, "numBuckets":true}}"
ObjectNode jsonFacet = JsonNodeFactory.instance.objectNode();
ObjectNode entriesCount = JsonNodeFactory.instance.objectNode();
entriesCount.put("type", "terms");
entriesCount.put("field", facetField + "_filter");
entriesCount.put("limit", 0);
entriesCount.put("numBuckets", true);
jsonFacet.set("entries_count", entriesCount);
if (StringUtils.isNotBlank(startsWith)) {
dff = new DiscoverFacetField(facetField,
DiscoveryConfigurationParameters.TYPE_TEXT, -1,
DiscoveryConfigurationParameters.SORT.VALUE, startsWith);
DiscoveryConfigurationParameters.TYPE_TEXT, limit,
DiscoveryConfigurationParameters.SORT.VALUE, startsWith, offset);
// Add the prefix to the json facet query
entriesCount.put("prefix", startsWith);
} else {
dff = new DiscoverFacetField(facetField,
DiscoveryConfigurationParameters.TYPE_TEXT, -1,
DiscoveryConfigurationParameters.SORT.VALUE);
DiscoveryConfigurationParameters.TYPE_TEXT, limit,
DiscoveryConfigurationParameters.SORT.VALUE, offset);
}
query.addFacetField(dff);
query.setFacetMinCount(1);
query.setMaxResults(0);
query.addProperty("json.facet", jsonFacet.toString());
} else {
query.setMaxResults(limit/* > 0 ? limit : 20*/);
if (offset > 0) {
@@ -248,8 +265,7 @@ public class SolrBrowseDAO implements BrowseDAO {
DiscoverResult resp = getSolrResponse();
int count = 0;
if (distinct) {
List<FacetResult> facetResults = resp.getFacetResult(facetField);
count = facetResults.size();
count = (int) resp.getTotalEntries();
} else {
// we need to cast to int to respect the BrowseDAO contract...
count = (int) resp.getTotalSearchResults();
@@ -266,8 +282,8 @@ public class SolrBrowseDAO implements BrowseDAO {
DiscoverResult resp = getSolrResponse();
List<FacetResult> facet = resp.getFacetResult(facetField);
int count = doCountQuery();
int start = offset > 0 ? offset : 0;
int max = limit > 0 ? limit : count; //if negative, return everything
int start = 0;
int max = facet.size();
List<String[]> result = new ArrayList<>();
if (ascending) {
for (int i = start; i < (start + max) && i < count; i++) {

View File

@@ -67,6 +67,7 @@ import org.dspace.event.Event;
import org.dspace.harvest.HarvestedItem;
import org.dspace.harvest.service.HarvestedItemService;
import org.dspace.identifier.DOI;
import org.dspace.identifier.DOIIdentifierProvider;
import org.dspace.identifier.IdentifierException;
import org.dspace.identifier.service.DOIService;
import org.dspace.identifier.service.IdentifierService;
@@ -81,6 +82,9 @@ import org.dspace.orcid.service.OrcidTokenService;
import org.dspace.profile.service.ResearcherProfileService;
import org.dspace.qaevent.dao.QAEventsDAO;
import org.dspace.services.ConfigurationService;
import org.dspace.versioning.Version;
import org.dspace.versioning.VersionHistory;
import org.dspace.versioning.service.VersionHistoryService;
import org.dspace.versioning.service.VersioningService;
import org.dspace.workflow.WorkflowItemService;
import org.dspace.workflow.factory.WorkflowServiceFactory;
@@ -176,6 +180,9 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl<Item> implements It
@Autowired
private QAEventsDAO qaEventsDao;
@Autowired
private VersionHistoryService versionHistoryService;
protected ItemServiceImpl() {
}
@@ -851,6 +858,7 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl<Item> implements It
DOI doi = doiService.findDOIByDSpaceObject(context, item);
if (doi != null) {
doi.setDSpaceObject(null);
doi.setStatus(DOIIdentifierProvider.TO_BE_DELETED);
}
// remove version attached to the item
@@ -1931,4 +1939,40 @@ prevent the generation of resource policy entry values with null dspace_object a
}
}
@Override
public boolean isLatestVersion(Context context, Item item) throws SQLException {
VersionHistory history = versionHistoryService.findByItem(context, item);
if (history == null) {
// not all items have a version history
// if an item does not have a version history, it is by definition the latest
// version
return true;
}
// start with the very latest version of the given item (may still be in
// workspace)
Version latestVersion = versionHistoryService.getLatestVersion(context, history);
// find the latest version of the given item that is archived
while (latestVersion != null && !latestVersion.getItem().isArchived()) {
latestVersion = versionHistoryService.getPrevious(context, history, latestVersion);
}
// could not find an archived version of the given item
if (latestVersion == null) {
// this scenario should never happen, but let's err on the side of showing too
// many items vs. to little
// (see discovery.xml, a lot of discovery configs filter out all items that are
// not the latest version)
return true;
}
// sanity check
assert latestVersion.getItem().isArchived();
return item.equals(latestVersion.getItem());
}
}

View File

@@ -178,6 +178,14 @@ public class WorkspaceItemServiceImpl implements WorkspaceItemService {
@Override
public WorkspaceItem create(Context c, WorkflowItem workflowItem) throws SQLException, AuthorizeException {
WorkspaceItem potentialDuplicate = findByItem(c, workflowItem.getItem());
if (potentialDuplicate != null) {
throw new IllegalArgumentException(String.format(
"A workspace item referring to item %s already exists (%d)",
workflowItem.getItem().getID(),
potentialDuplicate.getID()
));
}
WorkspaceItem workspaceItem = workspaceItemDAO.create(c, new WorkspaceItem());
workspaceItem.setItem(workflowItem.getItem());
workspaceItem.setCollection(workflowItem.getCollection());

View File

@@ -8,6 +8,7 @@
package org.dspace.content.authority;
import java.io.File;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -65,14 +66,17 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements Hiera
protected static String labelTemplate = "//node[@label = '%s']";
protected static String idParentTemplate = "//node[@id = '%s']/parent::isComposedBy/parent::node";
protected static String rootTemplate = "/node";
protected static String idAttribute = "id";
protected static String labelAttribute = "label";
protected static String pluginNames[] = null;
protected String vocabularyName = null;
protected InputSource vocabulary = null;
protected Boolean suggestHierarchy = false;
protected Boolean storeHierarchy = true;
protected String hierarchyDelimiter = "::";
protected Integer preloadLevel = 1;
protected String valueAttribute = labelAttribute;
protected String valueTemplate = labelTemplate;
public DSpaceControlledVocabulary() {
super();
@@ -115,7 +119,7 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements Hiera
}
}
protected void init() {
protected void init(String locale) {
if (vocabulary == null) {
ConfigurationService config = DSpaceServicesFactory.getInstance().getConfigurationService();
@@ -125,13 +129,25 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements Hiera
File.separator + "controlled-vocabularies" + File.separator;
String configurationPrefix = "vocabulary.plugin." + vocabularyName;
storeHierarchy = config.getBooleanProperty(configurationPrefix + ".hierarchy.store", storeHierarchy);
boolean storeIDs = config.getBooleanProperty(configurationPrefix + ".storeIDs", false);
suggestHierarchy = config.getBooleanProperty(configurationPrefix + ".hierarchy.suggest", suggestHierarchy);
preloadLevel = config.getIntProperty(configurationPrefix + ".hierarchy.preloadLevel", preloadLevel);
String configuredDelimiter = config.getProperty(configurationPrefix + ".delimiter");
if (configuredDelimiter != null) {
hierarchyDelimiter = configuredDelimiter.replaceAll("(^\"|\"$)", "");
}
if (storeIDs) {
valueAttribute = idAttribute;
valueTemplate = idTemplate;
}
String filename = vocabulariesPath + vocabularyName + ".xml";
if (StringUtils.isNotEmpty(locale)) {
String localizedFilename = vocabulariesPath + vocabularyName + "_" + locale + ".xml";
if (Paths.get(localizedFilename).toFile().exists()) {
filename = localizedFilename;
}
}
log.info("Loading " + filename);
vocabulary = new InputSource(filename);
}
@@ -144,9 +160,9 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements Hiera
return ("");
} else {
String parentValue = buildString(node.getParentNode());
Node currentLabel = node.getAttributes().getNamedItem("label");
if (currentLabel != null) {
String currentValue = currentLabel.getNodeValue();
Node currentNodeValue = node.getAttributes().getNamedItem(valueAttribute);
if (currentNodeValue != null) {
String currentValue = currentNodeValue.getNodeValue();
if (parentValue.equals("")) {
return currentValue;
} else {
@@ -160,12 +176,13 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements Hiera
@Override
public Choices getMatches(String text, int start, int limit, String locale) {
init();
init(locale);
log.debug("Getting matches for '" + text + "'");
String xpathExpression = "";
String[] textHierarchy = text.split(hierarchyDelimiter, -1);
for (int i = 0; i < textHierarchy.length; i++) {
xpathExpression += String.format(xpathTemplate, textHierarchy[i].replaceAll("'", "&apos;").toLowerCase());
xpathExpression +=
String.format(xpathTemplate, textHierarchy[i].replaceAll("'", "&apos;").toLowerCase());
}
XPath xpath = XPathFactory.newInstance().newXPath();
int total = 0;
@@ -184,12 +201,13 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements Hiera
@Override
public Choices getBestMatch(String text, String locale) {
init();
init(locale);
log.debug("Getting best matches for '" + text + "'");
String xpathExpression = "";
String[] textHierarchy = text.split(hierarchyDelimiter, -1);
for (int i = 0; i < textHierarchy.length; i++) {
xpathExpression += String.format(labelTemplate, textHierarchy[i].replaceAll("'", "&apos;"));
xpathExpression +=
String.format(valueTemplate, textHierarchy[i].replaceAll("'", "&apos;"));
}
XPath xpath = XPathFactory.newInstance().newXPath();
List<Choice> choices = new ArrayList<Choice>();
@@ -205,19 +223,19 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements Hiera
@Override
public String getLabel(String key, String locale) {
return getNodeLabel(key, this.suggestHierarchy);
return getNodeValue(key, locale, this.suggestHierarchy);
}
@Override
public String getValue(String key, String locale) {
return getNodeLabel(key, this.storeHierarchy);
return getNodeValue(key, locale, this.storeHierarchy);
}
@Override
public Choice getChoice(String authKey, String locale) {
Node node;
try {
node = getNode(authKey);
node = getNode(authKey, locale);
} catch (XPathExpressionException e) {
return null;
}
@@ -226,27 +244,27 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements Hiera
@Override
public boolean isHierarchical() {
init();
init(null);
return true;
}
@Override
public Choices getTopChoices(String authorityName, int start, int limit, String locale) {
init();
init(locale);
String xpathExpression = rootTemplate;
return getChoicesByXpath(xpathExpression, start, limit);
}
@Override
public Choices getChoicesByParent(String authorityName, String parentId, int start, int limit, String locale) {
init();
init(locale);
String xpathExpression = String.format(idTemplate, parentId);
return getChoicesByXpath(xpathExpression, start, limit);
}
@Override
public Choice getParentChoice(String authorityName, String childId, String locale) {
init();
init(locale);
try {
String xpathExpression = String.format(idParentTemplate, childId);
Choice choice = createChoiceFromNode(getNodeFromXPath(xpathExpression));
@@ -259,7 +277,7 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements Hiera
@Override
public Integer getPreloadLevel() {
init();
init(null);
return preloadLevel;
}
@@ -270,8 +288,8 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements Hiera
return false;
}
private Node getNode(String key) throws XPathExpressionException {
init();
private Node getNode(String key, String locale) throws XPathExpressionException {
init(locale);
String xpathExpression = String.format(idTemplate, key);
Node node = getNodeFromXPath(xpathExpression);
return node;
@@ -319,16 +337,16 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements Hiera
return extras;
}
private String getNodeLabel(String key, boolean useHierarchy) {
private String getNodeValue(String key, String locale, boolean useHierarchy) {
try {
Node node = getNode(key);
Node node = getNode(key, locale);
if (Objects.isNull(node)) {
return null;
}
if (useHierarchy) {
return this.buildString(node);
} else {
return node.getAttributes().getNamedItem("label").getNodeValue();
return node.getAttributes().getNamedItem(valueAttribute).getNodeValue();
}
} catch (XPathExpressionException e) {
return ("");
@@ -349,7 +367,7 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements Hiera
if (this.storeHierarchy) {
return hierarchy;
} else {
return node.getAttributes().getNamedItem("label").getNodeValue();
return node.getAttributes().getNamedItem(valueAttribute).getNodeValue();
}
}

View File

@@ -1009,4 +1009,14 @@ public interface ItemService
*/
EntityType getEntityType(Context context, Item item) throws SQLException;
/**
* Check whether the given item is the latest version. If the latest item cannot
* be determined, because either the version history or the latest version is
* not present, assume the item is latest.
* @param context the DSpace context.
* @param item the item that should be checked.
* @return true if the item is the latest version, false otherwise.
*/
public boolean isLatestVersion(Context context, Item item) throws SQLException;
}

View File

@@ -313,7 +313,7 @@ public abstract class AbstractHibernateDAO<T> implements GenericDAO<T> {
org.hibernate.query.Query hquery = query.unwrap(org.hibernate.query.Query.class);
Stream<T> stream = hquery.stream();
Iterator<T> iter = stream.iterator();
return new AbstractIterator<T> () {
return new AbstractIterator<T>() {
@Override
protected T computeNext() {
return iter.hasNext() ? iter.next() : endOfData();

View File

@@ -883,7 +883,19 @@ public class Context implements AutoCloseable {
}
/**
* Remove an entity from the cache. This is necessary when batch processing a large number of items.
* Remove all entities from the cache and reload the current user entity. This is useful when batch processing
* a large number of entities when the calling code requires the cache to be completely cleared before continuing.
*
* @throws SQLException if a database error occurs.
*/
public void uncacheEntities() throws SQLException {
dbConnection.uncacheEntities();
reloadContextBoundEntities();
}
/**
* Remove an entity from the cache. This is useful when batch processing a large number of entities
* when the calling code needs to retain some items in the cache while removing others.
*
* @param entity The entity to reload
* @param <E> The class of the entity. The entity must implement the {@link ReloadableEntity} interface.

View File

@@ -124,28 +124,38 @@ public interface DBConnection<T> {
public long getCacheSize() throws SQLException;
/**
* Reload a DSpace object from the database. This will make sure the object
* Reload an entity from the database. This will make sure the object
* is valid and stored in the cache. The returned object should be used
* henceforth instead of the passed object.
*
* @param <E> type of {@link entity}
* @param entity The DSpace object to reload
* @param <E> type of entity.
* @param entity The entity to reload.
* @return the reloaded entity.
* @throws java.sql.SQLException passed through.
* @throws SQLException passed through.
*/
public <E extends ReloadableEntity> E reloadEntity(E entity) throws SQLException;
/**
* Remove a DSpace object from the session cache when batch processing a
* large number of objects.
* Remove all entities from the session cache.
*
* <p>Objects removed from cache are not saved in any way. Therefore, if you
* have modified an object, you should be sure to {@link commit()} changes
* <p>Entities removed from cache are not saved in any way. Therefore, if you
* have modified any entities, you should be sure to {@link #commit()} changes
* before calling this method.
*
* @param <E> Type of {@link entity}
* @param entity The DSpace object to decache.
* @throws java.sql.SQLException passed through.
* @throws SQLException passed through.
*/
public void uncacheEntities() throws SQLException;
/**
* Remove an entity from the session cache.
*
* <p>Entities removed from cache are not saved in any way. Therefore, if you
* have modified the entity, you should be sure to {@link #commit()} changes
* before calling this method.
*
* @param <E> Type of entity.
* @param entity The entity to decache.
* @throws SQLException passed through.
*/
public <E extends ReloadableEntity> void uncacheEntity(E entity) throws SQLException;

View File

@@ -242,6 +242,11 @@ public class HibernateDBConnection implements DBConnection<Session> {
}
}
@Override
public void uncacheEntities() throws SQLException {
getSession().clear();
}
/**
* Evict an entity from the hibernate cache.
* <P>

View File

@@ -19,6 +19,8 @@ import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.curate.AbstractCurationTask;
import org.dspace.curate.Curator;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
/**
* A basic link checker that is designed to be extended. By default this link checker
@@ -42,6 +44,9 @@ public class BasicLinkChecker extends AbstractCurationTask {
// The log4j logger for this class
private static Logger log = org.apache.logging.log4j.LogManager.getLogger(BasicLinkChecker.class);
protected static final ConfigurationService configurationService
= DSpaceServicesFactory.getInstance().getConfigurationService();
/**
* Perform the link checking.
@@ -110,7 +115,8 @@ public class BasicLinkChecker extends AbstractCurationTask {
*/
protected boolean checkURL(String url, StringBuilder results) {
// Link check the URL
int httpStatus = getResponseStatus(url);
int redirects = 0;
int httpStatus = getResponseStatus(url, redirects);
if ((httpStatus >= 200) && (httpStatus < 300)) {
results.append(" - " + url + " = " + httpStatus + " - OK\n");
@@ -128,14 +134,24 @@ public class BasicLinkChecker extends AbstractCurationTask {
* @param url The url to open
* @return The HTTP response code (e.g. 200 / 301 / 404 / 500)
*/
protected int getResponseStatus(String url) {
protected int getResponseStatus(String url, int redirects) {
try {
URL theURL = new URL(url);
HttpURLConnection connection = (HttpURLConnection) theURL.openConnection();
int code = connection.getResponseCode();
connection.setInstanceFollowRedirects(true);
int statusCode = connection.getResponseCode();
int maxRedirect = configurationService.getIntProperty("curate.checklinks.max-redirect", 0);
if ((statusCode == HttpURLConnection.HTTP_MOVED_TEMP || statusCode == HttpURLConnection.HTTP_MOVED_PERM ||
statusCode == HttpURLConnection.HTTP_SEE_OTHER)) {
connection.disconnect();
String newUrl = connection.getHeaderField("Location");
if (newUrl != null && (maxRedirect >= redirects || maxRedirect == -1)) {
redirects++;
return getResponseStatus(newUrl, redirects);
}
return code;
}
return statusCode;
} catch (IOException ioe) {
// Must be a bad URL

View File

@@ -15,13 +15,12 @@ import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.collections4.ListUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Bitstream;
import org.dspace.content.Bundle;
import org.dspace.content.DSpaceObject;
@@ -99,8 +98,13 @@ public class ClamScan extends AbstractCurationTask {
}
try {
Bundle bundle = itemService.getBundles(item, "ORIGINAL").get(0);
results = new ArrayList<>();
List<Bundle> bundles = itemService.getBundles(item, "ORIGINAL");
if (ListUtils.emptyIfNull(bundles).isEmpty()) {
setResult("No ORIGINAL bundle found for item: " + getItemHandle(item));
return Curator.CURATE_SKIP;
}
Bundle bundle = bundles.get(0);
results = new ArrayList<String>();
for (Bitstream bitstream : bundle.getBitstreams()) {
InputStream inputstream = bitstreamService.retrieve(Curator.curationContext(), bitstream);
logDebugMessage("Scanning " + bitstream.getName() + " . . . ");
@@ -121,10 +125,11 @@ public class ClamScan extends AbstractCurationTask {
}
}
} catch (AuthorizeException authE) {
throw new IOException(authE.getMessage(), authE);
} catch (SQLException sqlE) {
throw new IOException(sqlE.getMessage(), sqlE);
} catch (Exception e) {
// Any exception which may occur during the performance of the task should be caught here
// And end the process gracefully
log.error("Error scanning item: " + getItemHandle(item), e);
status = Curator.CURATE_ERROR;
} finally {
closeSession();
}

View File

@@ -10,6 +10,7 @@ package org.dspace.ctask.general;
import java.io.IOException;
import java.sql.SQLException;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -25,7 +26,6 @@ import org.dspace.identifier.IdentifierProvider;
import org.dspace.identifier.VersionedHandleIdentifierProviderWithCanonicalHandles;
import org.dspace.identifier.factory.IdentifierServiceFactory;
import org.dspace.identifier.service.IdentifierService;
import org.dspace.services.factory.DSpaceServicesFactory;
/**
* Ensure that an object has all of the identifiers that it should, minting them
@@ -45,20 +45,6 @@ public class CreateMissingIdentifiers
return Curator.CURATE_SKIP;
}
// XXX Temporary escape when an incompatible provider is configured.
// XXX Remove this when the provider is fixed.
boolean compatible = DSpaceServicesFactory
.getInstance()
.getServiceManager()
.getServiceByName(
VersionedHandleIdentifierProviderWithCanonicalHandles.class.getCanonicalName(),
IdentifierProvider.class) == null;
if (!compatible) {
setResult("This task is not compatible with VersionedHandleIdentifierProviderWithCanonicalHandles");
return Curator.CURATE_ERROR;
}
// XXX End of escape
String typeText = Constants.typeText[dso.getType()];
// Get a Context
@@ -75,6 +61,18 @@ public class CreateMissingIdentifiers
.getInstance()
.getIdentifierService();
// XXX Temporary escape when an incompatible provider is configured.
// XXX Remove this when the provider is fixed.
List<IdentifierProvider> providerList = identifierService.getProviders();
boolean compatible =
providerList.stream().noneMatch(p -> p instanceof VersionedHandleIdentifierProviderWithCanonicalHandles);
if (!compatible) {
setResult("This task is not compatible with VersionedHandleIdentifierProviderWithCanonicalHandles");
return Curator.CURATE_ERROR;
}
// XXX End of escape
// Register any missing identifiers.
try {
identifierService.register(context, dso);

View File

@@ -165,7 +165,7 @@ public class Curation extends DSpaceRunnable<CurationScriptConfiguration> {
* End of curation script; logs script time if -v verbose is set
*
* @param timeRun Time script was started
* @throws SQLException If DSpace contextx can't complete
* @throws SQLException If DSpace context can't complete
*/
private void endScript(long timeRun) throws SQLException {
context.complete();
@@ -185,7 +185,7 @@ public class Curation extends DSpaceRunnable<CurationScriptConfiguration> {
Curator curator = new Curator(handler);
OutputStream reporterStream;
if (null == this.reporter) {
reporterStream = new NullOutputStream();
reporterStream = NullOutputStream.NULL_OUTPUT_STREAM;
} else if ("-".equals(this.reporter)) {
reporterStream = System.out;
} else {
@@ -300,9 +300,17 @@ public class Curation extends DSpaceRunnable<CurationScriptConfiguration> {
// scope
if (this.commandLine.getOptionValue('s') != null) {
this.scope = this.commandLine.getOptionValue('s');
if (this.scope != null && Curator.TxScope.valueOf(this.scope.toUpperCase()) == null) {
this.handler.logError("Bad transaction scope '" + this.scope + "': only 'object', 'curation' or " +
"'open' recognized");
boolean knownScope;
try {
Curator.TxScope.valueOf(this.scope.toUpperCase());
knownScope = true;
} catch (IllegalArgumentException | NullPointerException e) {
knownScope = false;
}
if (!knownScope) {
this.handler.logError("Bad transaction scope '"
+ this.scope
+ "': only 'object', 'curation' or 'open' recognized");
throw new IllegalArgumentException(
"Bad transaction scope '" + this.scope + "': only 'object', 'curation' or " +
"'open' recognized");

View File

@@ -32,6 +32,9 @@ public class DiscoverResult {
private List<IndexableObject> indexableObjects;
private Map<String, List<FacetResult>> facetResults;
// Total count of facet entries calculated for a metadata browsing query
private long totalEntries;
/**
* A map that contains all the documents sougth after, the key is a string representation of the Indexable Object
*/
@@ -64,6 +67,14 @@ public class DiscoverResult {
this.totalSearchResults = totalSearchResults;
}
public long getTotalEntries() {
return totalEntries;
}
public void setTotalEntries(long totalEntries) {
this.totalEntries = totalEntries;
}
public int getStart() {
return start;
}

View File

@@ -40,14 +40,13 @@ import org.dspace.services.factory.DSpaceServicesFactory;
import org.dspace.utils.DSpace;
/**
* Class used to reindex dspace communities/collections/items into discovery
* Class used to reindex DSpace communities/collections/items into discovery.
*/
public class IndexClient extends DSpaceRunnable<IndexDiscoveryScriptConfiguration> {
private Context context;
private IndexingService indexer = DSpaceServicesFactory.getInstance().getServiceManager()
.getServiceByName(IndexingService.class.getName(),
IndexingService.class);
.getServiceByName(IndexingService.class.getName(), IndexingService.class);
private IndexClientOptions indexClientOptions;
@@ -69,103 +68,80 @@ public class IndexClient extends DSpaceRunnable<IndexDiscoveryScriptConfiguratio
}
}
/** Acquire from dspace-services in future */
/**
* new DSpace.getServiceManager().getServiceByName("org.dspace.discovery.SolrIndexer");
*/
Optional<IndexableObject> indexableObject = Optional.empty();
if (indexClientOptions == IndexClientOptions.REMOVE || indexClientOptions == IndexClientOptions.INDEX) {
final String param = indexClientOptions == IndexClientOptions.REMOVE ? commandLine.getOptionValue('r') :
commandLine.getOptionValue('i');
UUID uuid = null;
try {
uuid = UUID.fromString(param);
} catch (Exception e) {
// nothing to do, it should be a handle
}
if (uuid != null) {
final Item item = ContentServiceFactory.getInstance().getItemService().find(context, uuid);
if (item != null) {
indexableObject = Optional.of(new IndexableItem(item));
} else {
// it could be a community
final Community community = ContentServiceFactory.getInstance().
getCommunityService().find(context, uuid);
if (community != null) {
indexableObject = Optional.of(new IndexableCommunity(community));
} else {
// it could be a collection
final Collection collection = ContentServiceFactory.getInstance().
getCollectionService().find(context, uuid);
if (collection != null) {
indexableObject = Optional.of(new IndexableCollection(collection));
}
}
}
} else {
final DSpaceObject dso = HandleServiceFactory.getInstance()
.getHandleService().resolveToObject(context, param);
if (dso != null) {
final IndexFactory indexableObjectService = IndexObjectFactoryFactory.getInstance().
getIndexFactoryByType(Constants.typeText[dso.getType()]);
indexableObject = indexableObjectService.findIndexableObject(context, dso.getID().toString());
}
}
final String param = indexClientOptions == IndexClientOptions.REMOVE ? commandLine.getOptionValue('r')
: commandLine.getOptionValue('i');
indexableObject = resolveIndexableObject(context, param);
if (!indexableObject.isPresent()) {
throw new IllegalArgumentException("Cannot resolve " + param + " to a DSpace object");
}
}
if (indexClientOptions == IndexClientOptions.REMOVE) {
switch (indexClientOptions) {
case REMOVE:
handler.logInfo("Removing " + commandLine.getOptionValue("r") + " from Index");
indexer.unIndexContent(context, indexableObject.get().getUniqueIndexID());
} else if (indexClientOptions == IndexClientOptions.CLEAN) {
break;
case CLEAN:
handler.logInfo("Cleaning Index");
indexer.cleanIndex();
} else if (indexClientOptions == IndexClientOptions.DELETE) {
break;
case DELETE:
handler.logInfo("Deleting Index");
indexer.deleteIndex();
} else if (indexClientOptions == IndexClientOptions.BUILD ||
indexClientOptions == IndexClientOptions.BUILDANDSPELLCHECK) {
break;
case BUILD:
case BUILDANDSPELLCHECK:
handler.logInfo("(Re)building index from scratch.");
if (StringUtils.isNotBlank(type)) {
handler.logWarning(String.format("Type option, %s, not applicable for entire index rebuild option, b" +
", type will be ignored", TYPE_OPTION));
handler.logWarning(String.format(
"Type option, %s, not applicable for entire index rebuild option, b"
+ ", type will be ignored",
TYPE_OPTION));
}
indexer.deleteIndex();
indexer.createIndex(context);
if (indexClientOptions == IndexClientOptions.BUILDANDSPELLCHECK) {
checkRebuildSpellCheck(commandLine, indexer);
}
} else if (indexClientOptions == IndexClientOptions.OPTIMIZE) {
break;
case OPTIMIZE:
handler.logInfo("Optimizing search core.");
indexer.optimize();
} else if (indexClientOptions == IndexClientOptions.SPELLCHECK) {
break;
case SPELLCHECK:
checkRebuildSpellCheck(commandLine, indexer);
} else if (indexClientOptions == IndexClientOptions.INDEX) {
break;
case INDEX:
handler.logInfo("Indexing " + commandLine.getOptionValue('i') + " force " + commandLine.hasOption("f"));
final long startTimeMillis = System.currentTimeMillis();
final long count = indexAll(indexer, ContentServiceFactory.getInstance().
getItemService(), context, indexableObject.get());
final long count = indexAll(indexer, ContentServiceFactory.getInstance().getItemService(), context,
indexableObject.get());
final long seconds = (System.currentTimeMillis() - startTimeMillis) / 1000;
handler.logInfo("Indexed " + count + " object" + (count > 1 ? "s" : "") + " in " + seconds + " seconds");
} else if (indexClientOptions == IndexClientOptions.UPDATE ||
indexClientOptions == IndexClientOptions.UPDATEANDSPELLCHECK) {
handler.logInfo("Indexed " + count + " object" + (count > 1 ? "s" : "") +
" in " + seconds + " seconds");
break;
case UPDATE:
case UPDATEANDSPELLCHECK:
handler.logInfo("Updating Index");
indexer.updateIndex(context, false, type);
if (indexClientOptions == IndexClientOptions.UPDATEANDSPELLCHECK) {
checkRebuildSpellCheck(commandLine, indexer);
}
} else if (indexClientOptions == IndexClientOptions.FORCEUPDATE ||
indexClientOptions == IndexClientOptions.FORCEUPDATEANDSPELLCHECK) {
break;
case FORCEUPDATE:
case FORCEUPDATEANDSPELLCHECK:
handler.logInfo("Updating Index");
indexer.updateIndex(context, true, type);
if (indexClientOptions == IndexClientOptions.FORCEUPDATEANDSPELLCHECK) {
checkRebuildSpellCheck(commandLine, indexer);
}
break;
default:
handler.handleException("Invalid index client option.");
break;
}
handler.logInfo("Done with indexing");
@@ -186,78 +162,93 @@ public class IndexClient extends DSpaceRunnable<IndexDiscoveryScriptConfiguratio
}
indexClientOptions = IndexClientOptions.getIndexClientOption(commandLine);
}
/**
* Indexes the given object and all children, if applicable.
* Resolves the given parameter to an IndexableObject (Item, Collection, or Community).
*
* @param indexingService
* @param itemService
* @param context The relevant DSpace Context.
* @param dso DSpace object to index recursively
* @throws IOException A general class of exceptions produced by failed or interrupted I/O operations.
* @throws SearchServiceException in case of a solr exception
* @throws SQLException An exception that provides information on a database access error or other errors.
* @param param The UUID or handle of the DSpace object.
* @return An Optional containing the IndexableObject if found.
* @throws SQLException If database error occurs.
*/
private static long indexAll(final IndexingService indexingService,
final ItemService itemService,
final Context context,
final IndexableObject dso)
throws IOException, SearchServiceException, SQLException {
private Optional<IndexableObject> resolveIndexableObject(Context context, String param) throws SQLException {
UUID uuid = null;
try {
uuid = UUID.fromString(param);
} catch (Exception e) {
// It's not a UUID, proceed to treat it as a handle.
}
if (uuid != null) {
Item item = ContentServiceFactory.getInstance().getItemService().find(context, uuid);
if (item != null) {
return Optional.of(new IndexableItem(item));
}
Community community = ContentServiceFactory.getInstance().getCommunityService().find(context, uuid);
if (community != null) {
return Optional.of(new IndexableCommunity(community));
}
Collection collection = ContentServiceFactory.getInstance().getCollectionService().find(context, uuid);
if (collection != null) {
return Optional.of(new IndexableCollection(collection));
}
} else {
DSpaceObject dso = HandleServiceFactory.getInstance().getHandleService().resolveToObject(context, param);
if (dso != null) {
IndexFactory indexableObjectService = IndexObjectFactoryFactory.getInstance()
.getIndexFactoryByType(Constants.typeText[dso.getType()]);
return indexableObjectService.findIndexableObject(context, dso.getID().toString());
}
}
return Optional.empty();
}
/**
* Indexes the given object and all its children recursively.
*
* @param indexingService The indexing service.
* @param itemService The item service.
* @param context The relevant DSpace Context.
* @param indexableObject The IndexableObject to index recursively.
* @return The count of indexed objects.
* @throws IOException If I/O error occurs.
* @throws SearchServiceException If a search service error occurs.
* @throws SQLException If database error occurs.
*/
private long indexAll(final IndexingService indexingService, final ItemService itemService, final Context context,
final IndexableObject indexableObject) throws IOException, SearchServiceException, SQLException {
long count = 0;
indexingService.indexContent(context, dso, true, true);
boolean commit = indexableObject instanceof IndexableCommunity ||
indexableObject instanceof IndexableCollection;
indexingService.indexContent(context, indexableObject, true, commit);
count++;
if (dso.getIndexedObject() instanceof Community) {
final Community community = (Community) dso.getIndexedObject();
if (indexableObject instanceof IndexableCommunity) {
final Community community = (Community) indexableObject.getIndexedObject();
final String communityHandle = community.getHandle();
for (final Community subcommunity : community.getSubcommunities()) {
count += indexAll(indexingService, itemService, context, new IndexableCommunity(subcommunity));
//To prevent memory issues, discard an object from the cache after processing
context.uncacheEntity(subcommunity);
}
// Reload community to get up-to-date collections
final Community reloadedCommunity = (Community) HandleServiceFactory.getInstance().getHandleService()
.resolveToObject(context,
communityHandle);
.resolveToObject(context, communityHandle);
for (final Collection collection : reloadedCommunity.getCollections()) {
count++;
indexingService.indexContent(context, new IndexableCollection(collection), true, true);
count += indexItems(indexingService, itemService, context, collection);
//To prevent memory issues, discard an object from the cache after processing
count += indexAll(indexingService, itemService, context, new IndexableCollection(collection));
context.uncacheEntity(collection);
}
} else if (dso instanceof IndexableCollection) {
count += indexItems(indexingService, itemService, context, (Collection) dso.getIndexedObject());
}
return count;
}
/**
* Indexes all items in the given collection.
*
* @param indexingService
* @param itemService
* @param context The relevant DSpace Context.
* @param collection collection to index
* @throws IOException A general class of exceptions produced by failed or interrupted I/O operations.
* @throws SearchServiceException in case of a solr exception
* @throws SQLException An exception that provides information on a database access error or other errors.
*/
private static long indexItems(final IndexingService indexingService,
final ItemService itemService,
final Context context,
final Collection collection)
throws IOException, SearchServiceException, SQLException {
long count = 0;
} else if (indexableObject instanceof IndexableCollection) {
final Collection collection = (Collection) indexableObject.getIndexedObject();
final Iterator<Item> itemIterator = itemService.findByCollection(context, collection);
while (itemIterator.hasNext()) {
Item item = itemIterator.next();
indexingService.indexContent(context, new IndexableItem(item), true, false);
count++;
//To prevent memory issues, discard an object from the cache after processing
context.uncacheEntity(item);
}
indexingService.commit();
}
return count;
}
@@ -268,7 +259,7 @@ public class IndexClient extends DSpaceRunnable<IndexDiscoveryScriptConfiguratio
* @param line the command line options
* @param indexer the solr indexer
* @throws SearchServiceException in case of a solr exception
* @throws IOException passed through
* @throws IOException If I/O error occurs.
*/
protected void checkRebuildSpellCheck(CommandLine line, IndexingService indexer)
throws SearchServiceException, IOException {

View File

@@ -41,6 +41,8 @@ import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.response.FacetField;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.response.json.BucketBasedJsonFacet;
import org.apache.solr.client.solrj.response.json.NestableJsonFacet;
import org.apache.solr.client.solrj.util.ClientUtils;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
@@ -1055,6 +1057,8 @@ public class SolrServiceImpl implements SearchService, IndexingService {
}
//Resolve our facet field values
resolveFacetFields(context, query, result, skipLoadingResponse, solrQueryResponse);
//Add total entries count for metadata browsing
resolveEntriesCount(result, solrQueryResponse);
}
// If any stale entries are found in the current page of results,
// we remove those stale entries and rerun the same query again.
@@ -1080,7 +1084,37 @@ public class SolrServiceImpl implements SearchService, IndexingService {
return result;
}
/**
* Stores the total count of entries for metadata index browsing. The count is calculated by the
* <code>json.facet</code> parameter with the following value:
*
* <pre><code>
* {
* "entries_count": {
* "type": "terms",
* "field": "facetNameField_filter",
* "limit": 0,
* "prefix": "prefix_value",
* "numBuckets": true
* }
* }
* </code></pre>
*
* This value is returned in the <code>facets</code> field of the Solr response.
*
* @param result DiscoverResult object where the total entries count will be stored
* @param solrQueryResponse QueryResponse object containing the solr response
*/
private void resolveEntriesCount(DiscoverResult result, QueryResponse solrQueryResponse) {
NestableJsonFacet response = solrQueryResponse.getJsonFacetingResponse();
if (response != null) {
BucketBasedJsonFacet facet = response.getBucketBasedFacets("entries_count");
if (facet != null) {
result.setTotalEntries(facet.getNumBucketsCount());
}
}
}
private void resolveFacetFields(Context context, DiscoverQuery query, DiscoverResult result,
boolean skipLoadingResponse, QueryResponse solrQueryResponse) throws SQLException {
@@ -1411,8 +1445,6 @@ public class SolrServiceImpl implements SearchService, IndexingService {
} else {
return field + "_acid";
}
} else if (facetFieldConfig.getType().equals(DiscoveryConfigurationParameters.TYPE_STANDARD)) {
return field;
} else {
return field;
}

View File

@@ -118,20 +118,10 @@ public abstract class IndexFactoryImpl<T extends IndexableObject, S> implements
ParseContext tikaContext = new ParseContext();
// Use Apache Tika to parse the full text stream(s)
boolean extractionSucceeded = false;
try (InputStream fullTextStreams = streams.getStream()) {
tikaParser.parse(fullTextStreams, tikaHandler, tikaMetadata, tikaContext);
// Write Tika metadata to "tika_meta_*" fields.
// This metadata is not very useful right now,
// but we'll keep it just in case it becomes more useful.
for (String name : tikaMetadata.names()) {
for (String value : tikaMetadata.getValues(name)) {
doc.addField("tika_meta_" + name, value);
}
}
// Save (parsed) full text to "fulltext" field
doc.addField("fulltext", tikaHandler.toString());
extractionSucceeded = true;
} catch (SAXException saxe) {
// Check if this SAXException is just a notice that this file was longer than the character limit.
// Unfortunately there is not a unique, public exception type to catch here. This error is thrown
@@ -141,6 +131,7 @@ public abstract class IndexFactoryImpl<T extends IndexableObject, S> implements
// log that we only indexed up to that configured limit
log.info("Full text is larger than the configured limit (discovery.solr.fulltext.charLimit)."
+ " Only the first {} characters were indexed.", charLimit);
extractionSucceeded = true;
} else {
log.error("Tika parsing error. Could not index full text.", saxe);
throw new IOException("Tika parsing error. Could not index full text.", saxe);
@@ -148,11 +139,19 @@ public abstract class IndexFactoryImpl<T extends IndexableObject, S> implements
} catch (TikaException | IOException ex) {
log.error("Tika parsing error. Could not index full text.", ex);
throw new IOException("Tika parsing error. Could not index full text.", ex);
} finally {
// Add document to index
solr.add(doc);
}
return;
if (extractionSucceeded) {
// Write Tika metadata to "tika_meta_*" fields.
// This metadata is not very useful right now,
// but we'll keep it just in case it becomes more useful.
for (String name : tikaMetadata.names()) {
for (String value : tikaMetadata.getValues(name)) {
doc.addField("tika_meta_" + name, value);
}
}
// Save (parsed) full text to "fulltext" field
doc.addField("fulltext", tikaHandler.toString());
}
}
// Add document to index
solr.add(doc);

View File

@@ -67,8 +67,6 @@ import org.dspace.handle.service.HandleService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.dspace.util.MultiFormatDateParser;
import org.dspace.util.SolrUtils;
import org.dspace.versioning.Version;
import org.dspace.versioning.VersionHistory;
import org.dspace.versioning.service.VersionHistoryService;
import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem;
import org.dspace.xmlworkflow.storedcomponents.service.XmlWorkflowItemService;
@@ -151,12 +149,14 @@ public class ItemIndexFactoryImpl extends DSpaceObjectIndexFactoryImpl<Indexable
doc.addField("withdrawn", item.isWithdrawn());
doc.addField("discoverable", item.isDiscoverable());
doc.addField("lastModified", SolrUtils.getDateFormatter().format(item.getLastModified()));
doc.addField("latestVersion", isLatestVersion(context, item));
doc.addField("latestVersion", itemService.isLatestVersion(context, item));
EPerson submitter = item.getSubmitter();
if (submitter != null) {
addFacetIndex(doc, "submitter", submitter.getID().toString(),
submitter.getFullName());
if (submitter != null && !(DSpaceServicesFactory.getInstance().getConfigurationService().getBooleanProperty(
"discovery.index.item.submitter.enabled", false))) {
doc.addField("submitter_authority", submitter.getID().toString());
} else if (submitter != null) {
addFacetIndex(doc, "submitter", submitter.getID().toString(), submitter.getFullName());
}
// Add the item metadata
@@ -175,43 +175,6 @@ public class ItemIndexFactoryImpl extends DSpaceObjectIndexFactoryImpl<Indexable
return doc;
}
/**
* Check whether the given item is the latest version.
* If the latest item cannot be determined, because either the version history or the latest version is not present,
* assume the item is latest.
* @param context the DSpace context.
* @param item the item that should be checked.
* @return true if the item is the latest version, false otherwise.
*/
protected boolean isLatestVersion(Context context, Item item) throws SQLException {
VersionHistory history = versionHistoryService.findByItem(context, item);
if (history == null) {
// not all items have a version history
// if an item does not have a version history, it is by definition the latest version
return true;
}
// start with the very latest version of the given item (may still be in workspace)
Version latestVersion = versionHistoryService.getLatestVersion(context, history);
// find the latest version of the given item that is archived
while (latestVersion != null && !latestVersion.getItem().isArchived()) {
latestVersion = versionHistoryService.getPrevious(context, history, latestVersion);
}
// could not find an archived version of the given item
if (latestVersion == null) {
// this scenario should never happen, but let's err on the side of showing too many items vs. to little
// (see discovery.xml, a lot of discovery configs filter out all items that are not the latest version)
return true;
}
// sanity check
assert latestVersion.getItem().isArchived();
return item.equals(latestVersion.getItem());
}
@Override
public SolrInputDocument buildNewDocument(Context context, IndexableItem indexableItem)
throws SQLException, IOException {
@@ -704,7 +667,7 @@ public class ItemIndexFactoryImpl extends DSpaceObjectIndexFactoryImpl<Indexable
return List.copyOf(workflowItemIndexFactory.getIndexableObjects(context, xmlWorkflowItem));
}
if (!isLatestVersion(context, item)) {
if (!itemService.isLatestVersion(context, item)) {
// the given item is an older version of another item
return List.of(new IndexableItem(item));
}

View File

@@ -15,6 +15,7 @@ import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import org.dspace.core.HibernateProxyHelper;
/**
@@ -23,7 +24,7 @@ import org.dspace.core.HibernateProxyHelper;
* @author kevinvandevelde at atmire.com
*/
@Entity
@Table(name = "group2groupcache")
@Table(name = "group2groupcache", uniqueConstraints = { @UniqueConstraint(columnNames = {"parent_id", "child_id"}) })
public class Group2GroupCache implements Serializable {
@Id

View File

@@ -20,6 +20,7 @@ import java.util.Set;
import java.util.UUID;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.SetUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
@@ -673,15 +674,14 @@ public class GroupServiceImpl extends DSpaceObjectServiceImpl<Group> implements
/**
* Regenerate the group cache AKA the group2groupcache table in the database -
* meant to be called when a group is added or removed from another group
* Returns a set with pairs of parent and child group UUIDs, representing the new cache table rows.
*
* @param context The relevant DSpace Context.
* @param flushQueries flushQueries Flush all pending queries
* @return Pairs of parent and child group UUID of the new cache.
* @throws SQLException An exception that provides information on a database access error or other errors.
*/
protected void rethinkGroupCache(Context context, boolean flushQueries) throws SQLException {
private Set<Pair<UUID, UUID>> computeNewCache(Context context, boolean flushQueries) throws SQLException {
Map<UUID, Set<UUID>> parents = new HashMap<>();
List<Pair<UUID, UUID>> group2groupResults = groupDAO.getGroup2GroupResults(context, flushQueries);
@@ -689,19 +689,8 @@ public class GroupServiceImpl extends DSpaceObjectServiceImpl<Group> implements
UUID parent = group2groupResult.getLeft();
UUID child = group2groupResult.getRight();
// if parent doesn't have an entry, create one
if (!parents.containsKey(parent)) {
Set<UUID> children = new HashSet<>();
// add child id to the list
children.add(child);
parents.put(parent, children);
} else {
// parent has an entry, now add the child to the parent's record
// of children
Set<UUID> children = parents.get(parent);
children.add(child);
}
parents.putIfAbsent(parent, new HashSet<>());
parents.get(parent).add(child);
}
// now parents is a hash of all of the IDs of groups that are parents
@@ -714,27 +703,42 @@ public class GroupServiceImpl extends DSpaceObjectServiceImpl<Group> implements
parent.getValue().addAll(myChildren);
}
// empty out group2groupcache table
group2GroupCacheDAO.deleteAll(context);
// write out new one
// write out new cache IN MEMORY ONLY and returns it
Set<Pair<UUID, UUID>> newCache = new HashSet<>();
for (Map.Entry<UUID, Set<UUID>> parent : parents.entrySet()) {
UUID key = parent.getKey();
for (UUID child : parent.getValue()) {
Group parentGroup = find(context, key);
Group childGroup = find(context, child);
if (parentGroup != null && childGroup != null && group2GroupCacheDAO
.find(context, parentGroup, childGroup) == null) {
Group2GroupCache group2GroupCache = group2GroupCacheDAO.create(context, new Group2GroupCache());
group2GroupCache.setParent(parentGroup);
group2GroupCache.setChild(childGroup);
group2GroupCacheDAO.save(context, group2GroupCache);
newCache.add(Pair.of(key, child));
}
}
return newCache;
}
/**
* Regenerate the group cache AKA the group2groupcache table in the database -
* meant to be called when a group is added or removed from another group
*
* @param context The relevant DSpace Context.
* @param flushQueries flushQueries Flush all pending queries
* @throws SQLException An exception that provides information on a database access error or other errors.
*/
protected void rethinkGroupCache(Context context, boolean flushQueries) throws SQLException {
// current cache in the database
Set<Pair<UUID, UUID>> oldCache = group2GroupCacheDAO.getCache(context);
// correct cache, computed from the Group table
Set<Pair<UUID, UUID>> newCache = computeNewCache(context, flushQueries);
SetUtils.SetView<Pair<UUID, UUID>> toDelete = SetUtils.difference(oldCache, newCache);
SetUtils.SetView<Pair<UUID, UUID>> toCreate = SetUtils.difference(newCache, oldCache);
for (Pair<UUID, UUID> pair : toDelete ) {
group2GroupCacheDAO.deleteFromCache(context, pair.getLeft(), pair.getRight());
}
for (Pair<UUID, UUID> pair : toCreate ) {
group2GroupCacheDAO.addToCache(context, pair.getLeft(), pair.getRight());
}
}

View File

@@ -9,7 +9,10 @@ package org.dspace.eperson.dao;
import java.sql.SQLException;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import org.apache.commons.lang3.tuple.Pair;
import org.dspace.core.Context;
import org.dspace.core.GenericDAO;
import org.dspace.eperson.Group;
@@ -25,13 +28,74 @@ import org.dspace.eperson.Group2GroupCache;
*/
public interface Group2GroupCacheDAO extends GenericDAO<Group2GroupCache> {
public List<Group2GroupCache> findByParent(Context context, Group group) throws SQLException;
/**
* Returns the current cache table as a set of UUID pairs.
* @param context The relevant DSpace Context.
* @return Set of UUID pairs, where the first element is the parent UUID and the second one is the child UUID.
* @throws SQLException An exception that provides information on a database access error or other errors.
*/
Set<Pair<UUID, UUID>> getCache(Context context) throws SQLException;
public List<Group2GroupCache> findByChildren(Context context, Iterable<Group> groups) throws SQLException;
/**
* Returns all cache entities that are children of a given parent Group entity.
* @param context The relevant DSpace Context.
* @param group Parent group to perform the search.
* @return List of cached groups that are children of the parent group.
* @throws SQLException An exception that provides information on a database access error or other errors.
*/
List<Group2GroupCache> findByParent(Context context, Group group) throws SQLException;
public Group2GroupCache findByParentAndChild(Context context, Group parent, Group child) throws SQLException;
/**
* Returns all cache entities that are parents of at least one group from a children groups list.
* @param context The relevant DSpace Context.
* @param groups Children groups to perform the search.
* @return List of cached groups that are parents of at least one group from the children groups list.
* @throws SQLException An exception that provides information on a database access error or other errors.
*/
List<Group2GroupCache> findByChildren(Context context, Iterable<Group> groups) throws SQLException;
public Group2GroupCache find(Context context, Group parent, Group child) throws SQLException;
/**
* Returns the cache entity given specific parent and child groups.
* @param context The relevant DSpace Context.
* @param parent Parent group.
* @param child Child gruoup.
* @return Cached group.
* @throws SQLException An exception that provides information on a database access error or other errors.
*/
Group2GroupCache findByParentAndChild(Context context, Group parent, Group child) throws SQLException;
public void deleteAll(Context context) throws SQLException;
/**
* Returns the cache entity given specific parent and child groups.
* @param context The relevant DSpace Context.
* @param parent Parent group.
* @param child Child gruoup.
* @return Cached group.
* @throws SQLException An exception that provides information on a database access error or other errors.
*/
Group2GroupCache find(Context context, Group parent, Group child) throws SQLException;
/**
* Completely deletes the current cache table.
* @param context The relevant DSpace Context.
* @throws SQLException An exception that provides information on a database access error or other errors.
*/
void deleteAll(Context context) throws SQLException;
/**
* Deletes a specific cache row given parent and child groups UUIDs.
* @param context The relevant DSpace Context.
* @param parent Parent group UUID.
* @param child Child group UUID.
* @throws SQLException An exception that provides information on a database access error or other errors.
*/
void deleteFromCache(Context context, UUID parent, UUID child) throws SQLException;
/**
* Adds a single row to the cache table given parent and child groups UUIDs.
* @param context The relevant DSpace Context.
* @param parent Parent group UUID.
* @param child Child group UUID.
* @throws SQLException An exception that provides information on a database access error or other errors.
*/
void addToCache(Context context, UUID parent, UUID child) throws SQLException;
}

View File

@@ -8,14 +8,18 @@
package org.dspace.eperson.dao.impl;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import jakarta.persistence.Query;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import org.apache.commons.lang3.tuple.Pair;
import org.dspace.core.AbstractHibernateDAO;
import org.dspace.core.Context;
import org.dspace.eperson.Group;
@@ -35,6 +39,16 @@ public class Group2GroupCacheDAOImpl extends AbstractHibernateDAO<Group2GroupCac
super();
}
@Override
public Set<Pair<UUID, UUID>> getCache(Context context) throws SQLException {
Query query = createQuery(
context,
"SELECT new org.apache.commons.lang3.tuple.ImmutablePair(g.parent.id, g.child.id) FROM Group2GroupCache g"
);
List<Pair<UUID, UUID>> results = query.getResultList();
return new HashSet<Pair<UUID, UUID>>(results);
}
@Override
public List<Group2GroupCache> findByParent(Context context, Group group) throws SQLException {
CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
@@ -90,4 +104,24 @@ public class Group2GroupCacheDAOImpl extends AbstractHibernateDAO<Group2GroupCac
public void deleteAll(Context context) throws SQLException {
createQuery(context, "delete from Group2GroupCache").executeUpdate();
}
@Override
public void deleteFromCache(Context context, UUID parent, UUID child) throws SQLException {
Query query = getHibernateSession(context).createNativeQuery(
"delete from group2groupcache g WHERE g.parent_id = :parent AND g.child_id = :child"
);
query.setParameter("parent", parent);
query.setParameter("child", child);
query.executeUpdate();
}
@Override
public void addToCache(Context context, UUID parent, UUID child) throws SQLException {
Query query = getHibernateSession(context).createNativeQuery(
"insert into group2groupcache (parent_id, child_id) VALUES (:parent, :child)"
);
query.setParameter("parent", parent);
query.setParameter("child", child);
query.executeUpdate();
}
}

View File

@@ -1,144 +0,0 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.google;
import java.io.File;
import java.util.HashSet;
import java.util.Set;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.analytics.Analytics;
import com.google.api.services.analytics.AnalyticsScopes;
import org.apache.logging.log4j.Logger;
import org.dspace.services.factory.DSpaceServicesFactory;
/**
* User: Robin Taylor
* Date: 11/07/2014
* Time: 13:23
*/
public class GoogleAccount {
// Read from config
private String applicationName;
private String tableId;
private String emailAddress;
private String certificateLocation;
// Created from factories
private JsonFactory jsonFactory;
private HttpTransport httpTransport;
// The Google stuff
private Credential credential;
private Analytics client;
private volatile static GoogleAccount uniqueInstance;
private static Logger log = org.apache.logging.log4j.LogManager.getLogger(GoogleAccount.class);
private GoogleAccount() {
applicationName = DSpaceServicesFactory.getInstance().getConfigurationService()
.getProperty("google-analytics.application.name");
tableId = DSpaceServicesFactory.getInstance().getConfigurationService()
.getProperty("google-analytics.table.id");
emailAddress = DSpaceServicesFactory.getInstance().getConfigurationService()
.getProperty("google-analytics.account.email");
certificateLocation = DSpaceServicesFactory.getInstance().getConfigurationService()
.getProperty("google-analytics.certificate.location");
jsonFactory = JacksonFactory.getDefaultInstance();
try {
httpTransport = GoogleNetHttpTransport.newTrustedTransport();
credential = authorize();
} catch (Exception e) {
throw new RuntimeException("Error initialising Google Analytics client", e);
}
// Create an Analytics instance
client = new Analytics.Builder(httpTransport, jsonFactory, credential).setApplicationName(applicationName)
.build();
log.info("Google Analytics client successfully initialised");
}
public static GoogleAccount getInstance() {
if (uniqueInstance == null) {
synchronized (GoogleAccount.class) {
if (uniqueInstance == null) {
uniqueInstance = new GoogleAccount();
}
}
}
return uniqueInstance;
}
private Credential authorize() throws Exception {
Set<String> scopes = new HashSet<String>();
scopes.add(AnalyticsScopes.ANALYTICS);
scopes.add(AnalyticsScopes.ANALYTICS_EDIT);
scopes.add(AnalyticsScopes.ANALYTICS_MANAGE_USERS);
scopes.add(AnalyticsScopes.ANALYTICS_PROVISION);
scopes.add(AnalyticsScopes.ANALYTICS_READONLY);
credential = new GoogleCredential.Builder()
.setTransport(httpTransport)
.setJsonFactory(jsonFactory)
.setServiceAccountId(emailAddress)
.setServiceAccountScopes(scopes)
.setServiceAccountPrivateKeyFromP12File(new File(certificateLocation))
.build();
return credential;
}
public String getApplicationName() {
return applicationName;
}
public String getTableId() {
return tableId;
}
public String getEmailAddress() {
return emailAddress;
}
public String getCertificateLocation() {
return certificateLocation;
}
public JsonFactory getJsonFactory() {
return jsonFactory;
}
public HttpTransport getHttpTransport() {
return httpTransport;
}
public Credential getCredential() {
return credential;
}
public Analytics getClient() {
return client;
}
}

View File

@@ -1,49 +0,0 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.google;
import java.io.IOException;
import com.google.api.services.analytics.model.GaData;
/**
* User: Robin Taylor
* Date: 20/08/2014
* Time: 09:26
*/
public class GoogleQueryManager {
public GaData getPageViews(String startDate, String endDate, String handle) throws IOException {
return GoogleAccount.getInstance().getClient().data().ga().get(
GoogleAccount.getInstance().getTableId(),
startDate,
endDate,
"ga:pageviews") // Metrics.
.setDimensions("ga:year,ga:month")
.setSort("-ga:year,-ga:month")
.setFilters("ga:pagePath=~/handle/" + handle + "$")
.execute();
}
public GaData getBitstreamDownloads(String startDate, String endDate, String handle) throws IOException {
return GoogleAccount.getInstance().getClient().data().ga().get(
GoogleAccount.getInstance().getTableId(),
startDate,
endDate,
"ga:totalEvents") // Metrics.
.setDimensions("ga:year,ga:month")
.setSort("-ga:year,-ga:month")
.setFilters(
"ga:eventCategory==bitstream;ga:eventAction==download;ga:pagePath=~" + handle + "/")
.execute();
}
}

View File

@@ -1,201 +0,0 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.google;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.logging.log4j.Logger;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.core.Constants;
import org.dspace.service.ClientInfoService;
import org.dspace.services.ConfigurationService;
import org.dspace.services.model.Event;
import org.dspace.usage.AbstractUsageEventListener;
import org.dspace.usage.UsageEvent;
import org.springframework.beans.factory.annotation.Autowired;
/**
* User: Robin Taylor
* Date: 14/08/2014
* Time: 10:05
*
* Notify Google Analytics of... well anything we want really.
* @deprecated Use org.dspace.google.GoogleAsyncEventListener instead
*/
@Deprecated
public class GoogleRecorderEventListener extends AbstractUsageEventListener {
private String analyticsKey;
private CloseableHttpClient httpclient;
private String GoogleURL = "https://www.google-analytics.com/collect";
private static Logger log = org.apache.logging.log4j.LogManager.getLogger(GoogleRecorderEventListener.class);
protected ContentServiceFactory contentServiceFactory;
protected ConfigurationService configurationService;
protected ClientInfoService clientInfoService;
public GoogleRecorderEventListener() {
// httpclient is threadsafe so we only need one.
httpclient = HttpClients.createDefault();
}
@Autowired
public void setContentServiceFactory(ContentServiceFactory contentServiceFactory) {
this.contentServiceFactory = contentServiceFactory;
}
@Autowired
public void setConfigurationService(ConfigurationService configurationService) {
this.configurationService = configurationService;
}
@Autowired
public void setClientInfoService(ClientInfoService clientInfoService) {
this.clientInfoService = clientInfoService;
}
@Override
public void receiveEvent(Event event) {
if ((event instanceof UsageEvent)) {
log.debug("Usage event received " + event.getName());
// This is a wee bit messy but these keys should be combined in future.
analyticsKey = configurationService.getProperty("google.analytics.key");
if (StringUtils.isNotBlank(analyticsKey)) {
try {
UsageEvent ue = (UsageEvent) event;
if (ue.getAction() == UsageEvent.Action.VIEW) {
if (ue.getObject().getType() == Constants.BITSTREAM) {
logEvent(ue, "bitstream", "download");
// Note: I've left this commented out code here to show how we could record page views
// as events,
// but since they are already taken care of by the Google Analytics Javascript there is
// not much point.
//} else if (ue.getObject().getType() == Constants.ITEM) {
// logEvent(ue, "item", "view");
//} else if (ue.getObject().getType() == Constants.COLLECTION) {
// logEvent(ue, "collection", "view");
//} else if (ue.getObject().getType() == Constants.COMMUNITY) {
// logEvent(ue, "community", "view");
}
}
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
}
private void logEvent(UsageEvent ue, String category, String action) throws IOException, SQLException {
HttpPost httpPost = new HttpPost(GoogleURL);
List<NameValuePair> nvps = new ArrayList<NameValuePair>();
nvps.add(new BasicNameValuePair("v", "1"));
nvps.add(new BasicNameValuePair("tid", analyticsKey));
// Client Id, should uniquely identify the user or device. If we have a session id for the user
// then lets use it, else generate a UUID.
if (ue.getRequest().getSession(false) != null) {
nvps.add(new BasicNameValuePair("cid", ue.getRequest().getSession().getId()));
} else {
nvps.add(new BasicNameValuePair("cid", UUID.randomUUID().toString()));
}
nvps.add(new BasicNameValuePair("t", "event"));
nvps.add(new BasicNameValuePair("uip", getIPAddress(ue.getRequest())));
nvps.add(new BasicNameValuePair("ua", ue.getRequest().getHeader("USER-AGENT")));
nvps.add(new BasicNameValuePair("dr", ue.getRequest().getHeader("referer")));
nvps.add(new BasicNameValuePair("dp", ue.getRequest().getRequestURI()));
nvps.add(new BasicNameValuePair("dt", getObjectName(ue)));
nvps.add(new BasicNameValuePair("ec", category));
nvps.add(new BasicNameValuePair("ea", action));
if (ue.getObject().getType() == Constants.BITSTREAM) {
// Bitstream downloads may occasionally be for collection or community images, so we need to label them
// with the parent object type.
nvps.add(new BasicNameValuePair("el", getParentType(ue)));
}
httpPost.setEntity(new UrlEncodedFormEntity(nvps));
try (CloseableHttpResponse response2 = httpclient.execute(httpPost)) {
// I can't find a list of what are acceptable responses, so I log the response but take no action.
log.debug("Google Analytics response is " + response2.getStatusLine());
}
log.debug("Posted to Google Analytics - " + ue.getRequest().getRequestURI());
}
private String getParentType(UsageEvent ue) {
try {
int parentType = contentServiceFactory.getDSpaceObjectService(ue.getObject())
.getParentObject(ue.getContext(), ue.getObject()).getType();
if (parentType == Constants.ITEM) {
return "item";
} else if (parentType == Constants.COLLECTION) {
return "collection";
} else if (parentType == Constants.COMMUNITY) {
return "community";
}
} catch (SQLException e) {
// This shouldn't merit interrupting the user's transaction so log the error and continue.
log.error(
"Error in Google Analytics recording - can't determine ParentObjectType for bitstream " + ue.getObject()
.getID());
e.printStackTrace();
}
return null;
}
private String getObjectName(UsageEvent ue) {
try {
if (ue.getObject().getType() == Constants.BITSTREAM) {
// For a bitstream download we really want to know the title of the owning item rather than the
// bitstream name.
return contentServiceFactory.getDSpaceObjectService(ue.getObject())
.getParentObject(ue.getContext(), ue.getObject()).getName();
} else {
return ue.getObject().getName();
}
} catch (SQLException e) {
// This shouldn't merit interrupting the user's transaction so log the error and continue.
log.error(
"Error in Google Analytics recording - can't determine ParentObjectName for bitstream " + ue.getObject()
.getID());
e.printStackTrace();
}
return null;
}
private String getIPAddress(HttpServletRequest request) {
return clientInfoService.getClientIp(request);
}
}

View File

@@ -57,6 +57,11 @@ public class IdentifierServiceImpl implements IdentifierService {
}
}
@Override
public List<IdentifierProvider> getProviders() {
return this.providers;
}
/**
* Reserves identifiers for the item
*

View File

@@ -355,7 +355,10 @@ public class VersionedDOIIdentifierProvider extends DOIIdentifierProvider implem
if (changed) {
try {
itemService.clearMetadata(c, item, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, Item.ANY);
// Checks if Array newIdentifiers is empty to avoid adding null values to the metadata field.
if (!newIdentifiers.isEmpty()) {
itemService.addMetadata(c, item, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null, newIdentifiers);
}
itemService.update(c, item);
} catch (SQLException ex) {
throw new RuntimeException("A problem with the database connection occurred.", ex);

View File

@@ -13,7 +13,6 @@ import java.io.PrintStream;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
@@ -227,8 +226,16 @@ public class DOIOrganiser {
}
for (DOI doi : dois) {
doi = context.reloadEntity(doi);
try {
organiser.reserve(doi);
context.uncacheEntity(doi);
context.commit();
} catch (RuntimeException e) {
System.err.format("DOI %s for object %s reservation failed, skipping: %s%n",
doi.getDSpaceObject().getID().toString(),
doi.getDoi(), e.getMessage());
context.rollback();
}
}
} catch (SQLException ex) {
System.err.println("Error in database connection:" + ex.getMessage());
@@ -245,14 +252,22 @@ public class DOIOrganiser {
+ "that could be registered.");
}
for (DOI doi : dois) {
doi = context.reloadEntity(doi);
try {
organiser.register(doi);
context.uncacheEntity(doi);
context.commit();
} catch (SQLException e) {
System.err.format("DOI %s for object %s registration failed, skipping: %s%n",
doi.getDSpaceObject().getID().toString(),
doi.getDoi(), e.getMessage());
context.rollback();
}
}
} catch (SQLException ex) {
System.err.println("Error in database connection:" + ex.getMessage());
System.err.format("Error in database connection: %s%n", ex.getMessage());
ex.printStackTrace(System.err);
} catch (DOIIdentifierException ex) {
System.err.println("Error registering DOI identifier:" + ex.getMessage());
} catch (RuntimeException ex) {
System.err.format("Error registering DOI identifier: %s%n", ex.getMessage());
}
}
@@ -268,8 +283,9 @@ public class DOIOrganiser {
}
for (DOI doi : dois) {
doi = context.reloadEntity(doi);
organiser.update(doi);
context.uncacheEntity(doi);
context.commit();
}
} catch (SQLException ex) {
System.err.println("Error in database connection:" + ex.getMessage());
@@ -286,12 +302,17 @@ public class DOIOrganiser {
+ "that could be deleted.");
}
Iterator<DOI> iterator = dois.iterator();
while (iterator.hasNext()) {
DOI doi = iterator.next();
iterator.remove();
for (DOI doi : dois) {
doi = context.reloadEntity(doi);
try {
organiser.delete(doi.getDoi());
context.uncacheEntity(doi);
context.commit();
} catch (SQLException e) {
System.err.format("DOI %s for object %s deletion failed, skipping: %s%n",
doi.getDSpaceObject().getID().toString(),
doi.getDoi(), e.getMessage());
context.rollback();
}
}
} catch (SQLException ex) {
System.err.println("Error in database connection:" + ex.getMessage());
@@ -401,12 +422,18 @@ public class DOIOrganiser {
/**
* Register DOI with the provider
* @param doiRow - doi to register
* @param filter - logical item filter to override
* @throws SQLException
* @throws DOIIdentifierException
* @param doiRow DOI to register
* @param filter logical item filter to override
* @throws IllegalArgumentException
* if {@link doiRow} does not name an Item.
* @throws IllegalStateException
* on invalid DOI.
* @throws RuntimeException
* on database error.
*/
public void register(DOI doiRow, Filter filter) throws SQLException, DOIIdentifierException {
public void register(DOI doiRow, Filter filter)
throws IllegalArgumentException, IllegalStateException,
RuntimeException {
DSpaceObject dso = doiRow.getDSpaceObject();
if (Constants.ITEM != dso.getType()) {
throw new IllegalArgumentException("Currenty DSpace supports DOIs for Items only.");
@@ -473,30 +500,33 @@ public class DOIOrganiser {
}
/**
* Register DOI with the provider
* @param doiRow - doi to register
* @throws SQLException
* @throws DOIIdentifierException
* Register DOI with the provider.
* @param doiRow DOI to register
* @throws IllegalArgumentException passed through.
* @throws IllegalStateException passed through.
* @throws RuntimeException passed through.
*/
public void register(DOI doiRow) throws SQLException, DOIIdentifierException {
public void register(DOI doiRow)
throws IllegalStateException, IllegalArgumentException,
RuntimeException {
register(doiRow, this.filter);
}
/**
* Reserve DOI with the provider,
* @param doiRow - doi to reserve
* @throws SQLException
* @throws DOIIdentifierException
*/
public void reserve(DOI doiRow) {
reserve(doiRow, this.filter);
}
/**
* Reserve DOI with the provider
* Reserve DOI with the provider.
* @param doiRow - doi to reserve
* @throws SQLException
* @throws DOIIdentifierException
* @param filter - Logical item filter to determine whether this
* identifier should be reserved online.
* @throws IllegalStateException on invalid DOI.
* @throws RuntimeException on database error.
*/
public void reserve(DOI doiRow, Filter filter) {
DSpaceObject dso = doiRow.getDSpaceObject();
@@ -577,7 +607,8 @@ public class DOIOrganiser {
}
} catch (IdentifierException ex) {
if (!(ex instanceof DOIIdentifierException)) {
LOG.error("It wasn't possible to register the identifier online. ", ex);
LOG.error("Registering DOI {} for object {}: the registrar returned an error.",
doiRow.getDoi(), dso.getID(), ex);
}
DOIIdentifierException doiIdentifierException = (DOIIdentifierException) ex;

View File

@@ -461,6 +461,10 @@ public class DataCiteConnector
log.warn("While reserving the DOI {}, we got a http status code "
+ "{} and the message \"{}\".",
doi, Integer.toString(resp.statusCode), resp.getContent());
Format format = Format.getCompactFormat();
format.setEncoding("UTF-8");
XMLOutputter xout = new XMLOutputter(format);
log.info("We send the following XML:\n{}", xout.outputString(root));
throw new DOIIdentifierException("Unable to parse an answer from "
+ "DataCite API. Please have a look into DSpace logs.",
DOIIdentifierException.BAD_ANSWER);
@@ -632,6 +636,14 @@ public class DataCiteConnector
return sendHttpRequest(httpget, doi);
}
/**
* Send a DataCite metadata document to the registrar.
*
* @param doi identify the object.
* @param metadataRoot describe the object. The root element of the document.
* @return the registrar's response.
* @throws DOIIdentifierException passed through.
*/
protected DataCiteResponse sendMetadataPostRequest(String doi, Element metadataRoot)
throws DOIIdentifierException {
Format format = Format.getCompactFormat();
@@ -640,6 +652,14 @@ public class DataCiteConnector
return sendMetadataPostRequest(doi, xout.outputString(new Document(metadataRoot)));
}
/**
* Send a DataCite metadata document to the registrar.
*
* @param doi identify the object.
* @param metadata describe the object.
* @return the registrar's response.
* @throws DOIIdentifierException passed through.
*/
protected DataCiteResponse sendMetadataPostRequest(String doi, String metadata)
throws DOIIdentifierException {
// post mds/metadata/
@@ -687,7 +707,7 @@ public class DataCiteConnector
* properties such as request URI and method type.
* @param doi DOI string to operate on
* @return response from DataCite
* @throws DOIIdentifierException if DOI error
* @throws DOIIdentifierException if registrar returns an error.
*/
protected DataCiteResponse sendHttpRequest(HttpUriRequest req, String doi)
throws DOIIdentifierException {

View File

@@ -6,17 +6,14 @@
* http://www.dspace.org/license/
*/
/**
* Make requests to the DOI registration angencies, f.e.to
* <a href='http://n2t.net/ezid/'>EZID</a> DOI service, and analyze the responses.
* Make requests to the DOI registration agencies and analyze the responses.
*
* <p>
* Use {@link org.dspace.identifier.ezid.EZIDRequestFactory#getInstance} to
* configure an {@link org.dspace.identifier.ezid.EZIDRequest}
* with your authority number and credentials. {@code EZIDRequest} encapsulates
* EZID's operations (lookup, create/mint, modify, delete...).
* An operation returns an {@link org.dspace.identifier.ezid.EZIDResponse} which
* gives easy access to EZID's status code and value, status of the underlying
* HTTP request, and key/value pairs found in the response body (if any).
* </p>
* {@link DOIOrganiser} is a tool for managing DOI registrations.
*
* <p>
* Classes specific to the <a href='https://datacite.org/'>DataCite</a>
* registrar are here. See {@link org.dspace.identifier.ezid} for the
* <a href='https://ezid.cdlib.org'>EZID</a> registrar.
*/
package org.dspace.identifier.doi;

View File

@@ -0,0 +1,21 @@
/**
* 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/
*/
/**
* DOI classes specific to the EZID registrar.
*
* <p>
* Use {@link org.dspace.identifier.ezid.EZIDRequestFactory#getInstance} to
* configure an {@link org.dspace.identifier.ezid.EZIDRequest}
* with your authority number and credentials. {@code EZIDRequest} encapsulates
* EZID's operations (lookup, create/mint, modify, delete...).
* An operation returns an {@link org.dspace.identifier.ezid.EZIDResponse} which
* gives easy access to EZID's status code and value, status of the underlying
* HTTP request, and key/value pairs found in the response body (if any).
* </p>
*/
package org.dspace.identifier.ezid;

View File

@@ -19,6 +19,7 @@ import org.dspace.identifier.Identifier;
import org.dspace.identifier.IdentifierException;
import org.dspace.identifier.IdentifierNotFoundException;
import org.dspace.identifier.IdentifierNotResolvableException;
import org.dspace.identifier.IdentifierProvider;
/**
* @author Fabio Bolognesi (fabio at atmire dot com)
@@ -194,4 +195,9 @@ public interface IdentifierService {
void delete(Context context, DSpaceObject dso, String identifier)
throws AuthorizeException, SQLException, IdentifierException;
/**
* Get List of currently enabled IdentifierProviders
* @return List of enabled IdentifierProvider objects.
*/
List<IdentifierProvider> getProviders();
}

View File

@@ -53,6 +53,16 @@ public class DataCiteImportMetadataSourceServiceImpl
@Autowired
private ConfigurationService configurationService;
private String entityFilterQuery;
public String getEntityFilterQuery() {
return entityFilterQuery;
}
public void setEntityFilterQuery(String entityFilterQuery) {
this.entityFilterQuery = entityFilterQuery;
}
@Override
public String getImportSource() {
return "datacite";
@@ -80,6 +90,9 @@ public class DataCiteImportMetadataSourceServiceImpl
if (StringUtils.isBlank(id)) {
id = query;
}
if (StringUtils.isNotBlank(getEntityFilterQuery())) {
id = id + " " + getEntityFilterQuery();
}
uriParameters.put("query", id);
uriParameters.put("page[size]", "1");
int timeoutMs = configurationService.getIntProperty("datacite.timeout", 180000);
@@ -118,6 +131,9 @@ public class DataCiteImportMetadataSourceServiceImpl
if (StringUtils.isBlank(id)) {
id = query;
}
if (StringUtils.isNotBlank(getEntityFilterQuery())) {
id = id + " " + getEntityFilterQuery();
}
uriParameters.put("query", id);
// start = current dspace page / datacite page number starting with 1
// dspace rounds up/down to the next configured pagination settings.

View File

@@ -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.importer.external.datacite;
import java.util.Map;
import jakarta.annotation.Resource;
import org.dspace.importer.external.metadatamapping.AbstractMetadataFieldMapping;
/**
* An implementation of {@link AbstractMetadataFieldMapping}
* Responsible for defining the mapping of the datacite metadatum fields on the DSpace metadatum fields
*
* @author Pasquale Cavallo (pasquale.cavallo at 4science dot it)
* @author Florian Gantner (florian.gantner@uni-bamberg.de)
*/
public class DataCiteProjectFieldMapping extends AbstractMetadataFieldMapping {
/**
* Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it
* only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over
* what metadatafield is generated.
*
* @param metadataFieldMap The map containing the link between retrieve metadata and metadata that will be set to
* the item.
*/
@Override
@Resource(name = "dataciteProjectMetadataFieldMap")
public void setMetadataFieldMap(Map metadataFieldMap) {
super.setMetadataFieldMap(metadataFieldMap);
}
}

View File

@@ -0,0 +1,24 @@
/**
* 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.importer.external.datacite;
/**
* Implements a data source for querying Datacite for specific for Project resourceTypes.
* This inherits the methods of DataCiteImportMetadataSourceServiceImpl
*
* @author Florian Gantner (florian.gantner@uni-bamberg.de)
*
*/
public class DataCiteProjectImportMetadataSourceServiceImpl
extends DataCiteImportMetadataSourceServiceImpl {
@Override
public String getImportSource() {
return "dataciteProject";
}
}

View File

@@ -26,7 +26,7 @@ import org.jdom2.xpath.XPathFactory;
* This contributor is able to concat multi value.
* Given a certain path, if it contains several nodes,
* the values of nodes will be concatenated into a single one.
* The concrete example we can see in the file wos-responce.xml in the <abstract_text> node,
* The concrete example we can see in the file wos-response.xml in the <abstract_text> node,
* which may contain several <p> paragraphs,
* this Contributor allows concatenating all <p> paragraphs. to obtain a single one.
*

View File

@@ -0,0 +1,86 @@
/**
* 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.importer.external.metadatamapping.transform;
import static java.util.Optional.ofNullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Optional;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.importer.external.metadatamapping.contributor.JsonPathMetadataProcessor;
import org.dspace.util.SimpleMapConverter;
/**
* This class is a Metadata processor from a structured JSON Metadata result
* and uses a SimpleMapConverter, with a mapping properties file
* to map to a single string value based on mapped keys.<br/>
* Like:<br/>
* <code>journal-article = Article<code/>
*
* @author paulo-graca
*
*/
public class StringJsonValueMappingMetadataProcessorService implements JsonPathMetadataProcessor {
private final static Logger log = LogManager.getLogger();
/**
* The value map converter.
* a list of values to map from
*/
private SimpleMapConverter valueMapConverter;
private String path;
@Override
public Collection<String> processMetadata(String json) {
JsonNode rootNode = convertStringJsonToJsonNode(json);
Optional<JsonNode> abstractNode = Optional.of(rootNode.at(path));
Collection<String> values = new ArrayList<>();
if (abstractNode.isPresent() && abstractNode.get().getNodeType().equals(JsonNodeType.STRING)) {
String stringValue = abstractNode.get().asText();
values.add(ofNullable(stringValue)
.map(value -> valueMapConverter != null ? valueMapConverter.getValue(value) : value)
.orElse(valueMapConverter.getValue(null)));
}
return values;
}
private JsonNode convertStringJsonToJsonNode(String json) {
ObjectMapper mapper = new ObjectMapper();
JsonNode body = null;
try {
body = mapper.readTree(json);
} catch (JsonProcessingException e) {
log.error("Unable to process json response.", e);
}
return body;
}
/* Getters and Setters */
public String convertType(String type) {
return valueMapConverter != null ? valueMapConverter.getValue(type) : type;
}
public void setValueMapConverter(SimpleMapConverter valueMapConverter) {
this.valueMapConverter = valueMapConverter;
}
public void setPath(String path) {
this.path = path;
}
}

View File

@@ -10,6 +10,7 @@ package org.dspace.orcid.client;
import java.util.List;
import java.util.Optional;
import org.dspace.orcid.OrcidToken;
import org.dspace.orcid.exception.OrcidClientException;
import org.dspace.orcid.model.OrcidTokenResponseDTO;
import org.orcid.jaxb.model.v3.release.record.Person;
@@ -161,4 +162,11 @@ public interface OrcidClient {
*/
OrcidResponse deleteByPutCode(String accessToken, String orcid, String putCode, String path);
/**
* Revokes the given {@param accessToken} with a POST method.
* @param orcidToken the access token to revoke
* @throws OrcidClientException if some error occurs during the search
*/
void revokeToken(OrcidToken orcidToken);
}

View File

@@ -42,6 +42,7 @@ import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.dspace.orcid.OrcidToken;
import org.dspace.orcid.exception.OrcidClientException;
import org.dspace.orcid.model.OrcidEntityType;
import org.dspace.orcid.model.OrcidProfileSectionType;
@@ -178,6 +179,16 @@ public class OrcidClientImpl implements OrcidClient {
return execute(buildDeleteUriRequest(accessToken, "/" + orcid + path + "/" + putCode), true);
}
@Override
public void revokeToken(OrcidToken orcidToken) {
List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("client_id", orcidConfiguration.getClientId()));
params.add(new BasicNameValuePair("client_secret", orcidConfiguration.getClientSecret()));
params.add(new BasicNameValuePair("token", orcidToken.getAccessToken()));
executeSuccessful(buildPostForRevokeToken(new UrlEncodedFormEntity(params, Charset.defaultCharset())));
}
@Override
public OrcidTokenResponseDTO getReadPublicAccessToken() {
return getClientCredentialsAccessToken("/read-public");
@@ -220,6 +231,14 @@ public class OrcidClientImpl implements OrcidClient {
.build();
}
private HttpUriRequest buildPostForRevokeToken(HttpEntity entity) {
return post(orcidConfiguration.getRevokeUrl())
.addHeader("Accept", "application/json")
.addHeader("Content-Type", "application/x-www-form-urlencoded")
.setEntity(entity)
.build();
}
private HttpUriRequest buildPutUriRequest(String accessToken, String relativePath, Object object) {
return put(orcidConfiguration.getApiUrl() + relativePath.trim())
.addHeader("Content-Type", "application/vnd.orcid+xml")
@@ -234,6 +253,24 @@ public class OrcidClientImpl implements OrcidClient {
.build();
}
private void executeSuccessful(HttpUriRequest httpUriRequest) {
try {
HttpClient client = HttpClientBuilder.create().build();
HttpResponse response = client.execute(httpUriRequest);
if (isNotSuccessfull(response)) {
throw new OrcidClientException(
getStatusCode(response),
"Operation " + httpUriRequest.getMethod() + " for the resource " + httpUriRequest.getURI() +
" was not successful: " + new String(response.getEntity().getContent().readAllBytes(),
StandardCharsets.UTF_8)
);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private <T> T executeAndParseJson(HttpUriRequest httpUriRequest, Class<T> clazz) {
HttpClient client = HttpClientBuilder.create().build();

View File

@@ -35,6 +35,8 @@ public final class OrcidConfiguration {
private String scopes;
private String revokeUrl;
public String getApiUrl() {
return apiUrl;
}
@@ -111,4 +113,11 @@ public final class OrcidConfiguration {
return !StringUtils.isAnyBlank(clientId, clientSecret);
}
public String getRevokeUrl() {
return revokeUrl;
}
public void setRevokeUrl(String revokeUrl) {
this.revokeUrl = revokeUrl;
}
}

View File

@@ -14,9 +14,10 @@ import static java.util.Comparator.nullsFirst;
import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -82,7 +83,7 @@ public class OrcidQueueConsumer implements Consumer {
private RelationshipService relationshipService;
private final List<UUID> alreadyConsumedItems = new ArrayList<>();
private final Set<UUID> itemsToConsume = new HashSet<>();
@Override
public void initialize() throws Exception {
@@ -117,10 +118,16 @@ public class OrcidQueueConsumer implements Consumer {
return;
}
if (alreadyConsumedItems.contains(item.getID())) {
return;
itemsToConsume.add(item.getID());
}
@Override
public void end(Context context) throws Exception {
for (UUID itemId : itemsToConsume) {
Item item = itemService.find(context, itemId);
context.turnOffAuthorisationSystem();
try {
consumeItem(context, item);
@@ -130,6 +137,9 @@ public class OrcidQueueConsumer implements Consumer {
}
itemsToConsume.clear();
}
/**
* Consume the item if it is a profile or an ORCID entity.
*/
@@ -146,7 +156,7 @@ public class OrcidQueueConsumer implements Consumer {
consumeProfile(context, item);
}
alreadyConsumedItems.add(item.getID());
itemsToConsume.add(item.getID());
}
@@ -169,6 +179,10 @@ public class OrcidQueueConsumer implements Consumer {
continue;
}
if (isNotLatestVersion(context, entity)) {
continue;
}
orcidQueueService.create(context, relatedItem, entity);
}
@@ -329,6 +343,14 @@ public class OrcidQueueConsumer implements Consumer {
return !getProfileType().equals(itemService.getEntityTypeLabel(profileItemItem));
}
private boolean isNotLatestVersion(Context context, Item entity) {
try {
return !itemService.isLatestVersion(context, entity);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private String getMetadataValue(Item item, String metadataField) {
return itemService.getMetadataFirstValue(item, new MetadataFieldName(metadataField), Item.ANY);
}
@@ -345,11 +367,6 @@ public class OrcidQueueConsumer implements Consumer {
return !configurationService.getBooleanProperty("orcid.synchronization-enabled", true);
}
@Override
public void end(Context context) throws Exception {
alreadyConsumedItems.clear();
}
@Override
public void finish(Context context) throws Exception {
// nothing to do

View File

@@ -74,6 +74,16 @@ public interface OrcidQueueDAO extends GenericDAO<OrcidQueue> {
*/
public List<OrcidQueue> findByProfileItemOrEntity(Context context, Item item) throws SQLException;
/**
* Get the OrcidQueue records where the given item is the entity.
*
* @param context DSpace context object
* @param item the item to search for
* @return the found OrcidQueue entities
* @throws SQLException if database error
*/
public List<OrcidQueue> findByEntity(Context context, Item item) throws SQLException;
/**
* Find all the OrcidQueue records with the given entity and record type.
*

View File

@@ -63,6 +63,13 @@ public class OrcidQueueDAOImpl extends AbstractHibernateDAO<OrcidQueue> implemen
return query.getResultList();
}
@Override
public List<OrcidQueue> findByEntity(Context context, Item item) throws SQLException {
Query query = createQuery(context, "FROM OrcidQueue WHERE entity.id = :itemId");
query.setParameter("itemId", item.getID());
return query.getResultList();
}
@Override
public List<OrcidQueue> findByEntityAndRecordType(Context context, Item entity, String type) throws SQLException {
Query query = createQuery(context, "FROM OrcidQueue WHERE entity = :entity AND recordType = :type");

View File

@@ -164,6 +164,16 @@ public interface OrcidQueueService {
*/
public List<OrcidQueue> findByProfileItemOrEntity(Context context, Item item) throws SQLException;
/**
* Get the OrcidQueue records where the given item is the entity.
*
* @param context DSpace context object
* @param item the item to search for
* @return the found OrcidQueue records
* @throws SQLException if database error
*/
public List<OrcidQueue> findByEntity(Context context, Item item) throws SQLException;
/**
* Get all the OrcidQueue records with attempts less than the given attempts.
*

View File

@@ -70,6 +70,11 @@ public class OrcidQueueServiceImpl implements OrcidQueueService {
return orcidQueueDAO.findByProfileItemOrEntity(context, item);
}
@Override
public List<OrcidQueue> findByEntity(Context context, Item item) throws SQLException {
return orcidQueueDAO.findByEntity(context, item);
}
@Override
public long countByProfileItemId(Context context, UUID profileItemId) throws SQLException {
return orcidQueueDAO.countByProfileItemId(context, profileItemId);

View File

@@ -38,6 +38,7 @@ import org.dspace.eperson.EPerson;
import org.dspace.eperson.service.EPersonService;
import org.dspace.orcid.OrcidQueue;
import org.dspace.orcid.OrcidToken;
import org.dspace.orcid.client.OrcidClient;
import org.dspace.orcid.model.OrcidEntityType;
import org.dspace.orcid.model.OrcidTokenResponseDTO;
import org.dspace.orcid.service.OrcidQueueService;
@@ -49,6 +50,8 @@ import org.dspace.profile.OrcidProfileSyncPreference;
import org.dspace.profile.OrcidSynchronizationMode;
import org.dspace.profile.service.ResearcherProfileService;
import org.dspace.services.ConfigurationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
/**
@@ -59,6 +62,8 @@ import org.springframework.beans.factory.annotation.Autowired;
*/
public class OrcidSynchronizationServiceImpl implements OrcidSynchronizationService {
private static final Logger log = LoggerFactory.getLogger(OrcidSynchronizationServiceImpl.class);
@Autowired
private ItemService itemService;
@@ -80,6 +85,9 @@ public class OrcidSynchronizationServiceImpl implements OrcidSynchronizationServ
@Autowired
private ResearcherProfileService researcherProfileService;
@Autowired
private OrcidClient orcidClient;
@Override
public void linkProfile(Context context, Item profile, OrcidTokenResponseDTO token) throws SQLException {
@@ -118,24 +126,11 @@ public class OrcidSynchronizationServiceImpl implements OrcidSynchronizationServ
@Override
public void unlinkProfile(Context context, Item profile) throws SQLException {
clearOrcidProfileMetadata(context, profile);
String orcid = itemService.getMetadataFirstValue(profile, "person", "identifier", "orcid", Item.ANY);
itemService.clearMetadata(context, profile, "person", "identifier", "orcid", Item.ANY);
itemService.clearMetadata(context, profile, "dspace", "orcid", "scope", Item.ANY);
itemService.clearMetadata(context, profile, "dspace", "orcid", "authenticated", Item.ANY);
if (!configurationService.getBooleanProperty("orcid.disconnection.remain-sync", false)) {
clearSynchronizationSettings(context, profile);
}
EPerson eperson = ePersonService.findByNetid(context, orcid);
if (eperson != null ) {
eperson.setNetid(null);
updateEPerson(context, eperson);
}
orcidTokenService.deleteByProfileItem(context, profile);
clearOrcidToken(context, profile);
updateItem(context, profile);
@@ -146,6 +141,23 @@ public class OrcidSynchronizationServiceImpl implements OrcidSynchronizationServ
}
private void clearOrcidToken(Context context, Item profile) {
OrcidToken profileToken = orcidTokenService.findByProfileItem(context, profile);
if (profileToken == null) {
log.warn("Cannot find any token related to the user profile: {}", profile.getID());
return;
}
orcidTokenService.deleteByProfileItem(context, profile);
orcidClient.revokeToken(profileToken);
}
private void clearOrcidProfileMetadata(Context context, Item profile) throws SQLException {
itemService.clearMetadata(context, profile, "person", "identifier", "orcid", Item.ANY);
itemService.clearMetadata(context, profile, "dspace", "orcid", "scope", Item.ANY);
itemService.clearMetadata(context, profile, "dspace", "orcid", "authenticated", Item.ANY);
}
@Override
public boolean setEntityPreference(Context context, Item profile, OrcidEntityType type,
OrcidEntitySyncPreference value) throws SQLException {
@@ -291,6 +303,11 @@ public class OrcidSynchronizationServiceImpl implements OrcidSynchronizationServ
private void clearSynchronizationSettings(Context context, Item profile)
throws SQLException {
if (configurationService.getBooleanProperty("orcid.disconnection.remain-sync", false)) {
return;
}
itemService.clearMetadata(context, profile, "dspace", "orcid", "sync-mode", Item.ANY);
itemService.clearMetadata(context, profile, "dspace", "orcid", "sync-profile", Item.ANY);

View File

@@ -243,7 +243,7 @@ public class RDFConsumer implements Consumer {
DSOIdentifier id = new DSOIdentifier(dso, ctx);
// If an item gets withdrawn, a MODIFY event is fired. We have to
// delete the item from the triple store instead of converting it.
// we don't have to take care for reinstantions of items as they can
// we don't have to take care for reinstate events on items as they can
// be processed as normal modify events.
if (dso instanceof Item
&& event.getDetail() != null

View File

@@ -45,8 +45,8 @@ import org.dspace.core.Context;
import org.dspace.core.LogHelper;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.eperson.service.EPersonService;
import org.dspace.scripts.service.ProcessService;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
/**
@@ -72,7 +72,7 @@ public class ProcessServiceImpl implements ProcessService {
private MetadataFieldService metadataFieldService;
@Autowired
private EPersonService ePersonService;
private ConfigurationService configurationService;
@Override
public Process create(Context context, EPerson ePerson, String scriptName,
@@ -293,8 +293,8 @@ public class ProcessServiceImpl implements ProcessService {
@Override
public void appendLog(int processId, String scriptName, String output, ProcessLogLevel processLogLevel)
throws IOException {
File tmpDir = FileUtils.getTempDirectory();
File tempFile = new File(tmpDir, scriptName + processId + ".log");
File logsDir = getLogsDirectory();
File tempFile = new File(logsDir, processId + "-" + scriptName + ".log");
FileWriter out = new FileWriter(tempFile, true);
try {
try (BufferedWriter writer = new BufferedWriter(out)) {
@@ -309,13 +309,16 @@ public class ProcessServiceImpl implements ProcessService {
@Override
public void createLogBitstream(Context context, Process process)
throws IOException, SQLException, AuthorizeException {
File tmpDir = FileUtils.getTempDirectory();
File tempFile = new File(tmpDir, process.getName() + process.getID() + ".log");
File logsDir = getLogsDirectory();
File tempFile = new File(logsDir, process.getID() + "-" + process.getName() + ".log");
if (tempFile.exists()) {
FileInputStream inputStream = FileUtils.openInputStream(tempFile);
appendFile(context, process, inputStream, Process.OUTPUT_TYPE, process.getName() + process.getID() + ".log");
appendFile(context, process, inputStream, Process.OUTPUT_TYPE,
process.getID() + "-" + process.getName() + ".log");
inputStream.close();
tempFile.delete();
}
}
@Override
public List<Process> findByStatusAndCreationTimeOlderThan(Context context, List<ProcessStatus> statuses,
@@ -328,6 +331,23 @@ public class ProcessServiceImpl implements ProcessService {
return processDAO.countByUser(context, user);
}
@Override
public void failRunningProcesses(Context context) throws SQLException, IOException, AuthorizeException {
List<Process> processesToBeFailed = findByStatusAndCreationTimeOlderThan(
context, List.of(ProcessStatus.RUNNING, ProcessStatus.SCHEDULED), new Date());
for (Process process : processesToBeFailed) {
context.setCurrentUser(process.getEPerson());
// Fail the process.
log.info("Process with ID {} did not complete before tomcat shutdown, failing it now.", process.getID());
fail(context, process);
// But still attach its log to the process.
appendLog(process.getID(), process.getName(),
"Process did not complete before tomcat shutdown.",
ProcessLogLevel.ERROR);
createLogBitstream(context, process);
}
}
private String formatLogLine(int processId, String scriptName, String output, ProcessLogLevel processLogLevel) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
StringBuilder sb = new StringBuilder();
@@ -343,4 +363,15 @@ public class ProcessServiceImpl implements ProcessService {
return sb.toString();
}
private File getLogsDirectory() {
String pathStr = configurationService.getProperty("dspace.dir")
+ File.separator + "log" + File.separator + "processes";
File logsDir = new File(pathStr);
if (!logsDir.exists()) {
if (!logsDir.mkdirs()) {
throw new RuntimeException("Couldn't create [dspace.dir]/log/processes/ directory.");
}
}
return logsDir;
}
}

View File

@@ -277,4 +277,14 @@ public interface ProcessService {
* @throws SQLException If something goes wrong
*/
int countByUser(Context context, EPerson user) throws SQLException;
/**
* Cleans up running processes by failing them an attaching their logs to the process objects.
*
* @param context The DSpace context
* @throws SQLException
* @throws IOException
* @throws AuthorizeException
*/
void failRunningProcesses(Context context) throws SQLException, IOException, AuthorizeException;
}

View File

@@ -357,7 +357,7 @@ public class StatisticsImporter {
SolrInputDocument sid = new SolrInputDocument();
sid.addField("ip", ip);
sid.addField("type", dso.getType());
sid.addField("id", dso.getID());
sid.addField("id", dso.getID().toString());
sid.addField("time", DateFormatUtils.format(date, SolrLoggerServiceImpl.DATE_FORMAT_8601));
sid.addField("continent", continent);
sid.addField("country", country);
@@ -471,13 +471,13 @@ public class StatisticsImporter {
boolean verbose = line.hasOption('v');
// Find our solr server
String sserver = configurationService.getProperty("solr-statistics", "server");
String sserver = configurationService.getProperty("solr-statistics.server");
if (verbose) {
System.out.println("Writing to solr server at: " + sserver);
}
solr = new HttpSolrClient.Builder(sserver).build();
String dbPath = configurationService.getProperty("usage-statistics", "dbfile");
String dbPath = configurationService.getProperty("usage-statistics.dbfile");
try {
File dbFile = new File(dbPath);
geoipLookup = new DatabaseReader.Builder(dbFile).build();
@@ -492,6 +492,11 @@ public class StatisticsImporter {
"Unable to load GeoLite Database file (" + dbPath + ")! You may need to reinstall it. See the DSpace " +
"installation instructions for more details.",
e);
} catch (NullPointerException e) {
log.error(
"The value of the property usage-statistics.dbfile is null. You may need to install the GeoLite " +
"Database file and/or uncomment the property in the config file!",
e);
}

View File

@@ -25,7 +25,7 @@
* {@code EventService}, as with the stock listeners.
* </p>
*
* @see org.dspace.google.GoogleRecorderEventListener
* @see org.dspace.google.GoogleAsyncEventListener
* @see org.dspace.statistics.SolrLoggerUsageEventListener
*/

View File

@@ -33,6 +33,11 @@ import org.dspace.core.Context;
import org.dspace.discovery.IndexEventConsumer;
import org.dspace.event.Consumer;
import org.dspace.event.Event;
import org.dspace.orcid.OrcidHistory;
import org.dspace.orcid.OrcidQueue;
import org.dspace.orcid.factory.OrcidServiceFactory;
import org.dspace.orcid.service.OrcidHistoryService;
import org.dspace.orcid.service.OrcidQueueService;
import org.dspace.versioning.factory.VersionServiceFactory;
import org.dspace.versioning.service.VersionHistoryService;
import org.dspace.versioning.utils.RelationshipVersioningUtils;
@@ -58,6 +63,8 @@ public class VersioningConsumer implements Consumer {
private RelationshipTypeService relationshipTypeService;
private RelationshipService relationshipService;
private RelationshipVersioningUtils relationshipVersioningUtils;
private OrcidQueueService orcidQueueService;
private OrcidHistoryService orcidHistoryService;
@Override
public void initialize() throws Exception {
@@ -67,6 +74,8 @@ public class VersioningConsumer implements Consumer {
relationshipTypeService = ContentServiceFactory.getInstance().getRelationshipTypeService();
relationshipService = ContentServiceFactory.getInstance().getRelationshipService();
relationshipVersioningUtils = VersionServiceFactory.getInstance().getRelationshipVersioningUtils();
this.orcidQueueService = OrcidServiceFactory.getInstance().getOrcidQueueService();
this.orcidHistoryService = OrcidServiceFactory.getInstance().getOrcidHistoryService();
}
@Override
@@ -132,7 +141,8 @@ public class VersioningConsumer implements Consumer {
// unarchive previous item
unarchiveItem(ctx, previousItem);
// handles versions for ORCID publications waiting to be shipped, or already published (history-queue).
handleOrcidSynchronization(ctx, previousItem, latestItem);
// update relationships
updateRelationships(ctx, latestItem, previousItem);
}
@@ -148,6 +158,29 @@ public class VersioningConsumer implements Consumer {
));
}
private void handleOrcidSynchronization(Context ctx, Item previousItem, Item latestItem) {
try {
replaceOrcidHistoryEntities(ctx, previousItem, latestItem);
removeOrcidQueueEntries(ctx, previousItem);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private void removeOrcidQueueEntries(Context ctx, Item previousItem) throws SQLException {
List<OrcidQueue> queueEntries = orcidQueueService.findByEntity(ctx, previousItem);
for (OrcidQueue queueEntry : queueEntries) {
orcidQueueService.delete(ctx, queueEntry);
}
}
private void replaceOrcidHistoryEntities(Context ctx, Item previousItem, Item latestItem) throws SQLException {
List<OrcidHistory> entries = orcidHistoryService.findByEntity(ctx, previousItem);
for (OrcidHistory entry : entries) {
entry.setEntity(latestItem);
}
}
/**
* Update {@link Relationship#latestVersionStatus} of the relationships of both the old version and the new version
* of the item.

View File

@@ -8,6 +8,7 @@
package org.dspace.xmlworkflow.state.actions.processingaction;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -20,6 +21,8 @@ import org.dspace.app.util.Util;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.MetadataFieldName;
import org.dspace.core.Context;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.dspace.xmlworkflow.service.WorkflowRequirementsService;
import org.dspace.xmlworkflow.state.Step;
import org.dspace.xmlworkflow.state.actions.ActionAdvancedInfo;
@@ -34,6 +37,9 @@ import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem;
public class ScoreReviewAction extends ProcessingAction {
private static final Logger log = LogManager.getLogger(ScoreReviewAction.class);
private final ConfigurationService configurationService
= DSpaceServicesFactory.getInstance().getConfigurationService();
// Option(s)
public static final String SUBMIT_SCORE = "submit_score";
@@ -114,7 +120,14 @@ public class ScoreReviewAction extends ProcessingAction {
@Override
public List<String> getOptions() {
return List.of(SUBMIT_SCORE, RETURN_TO_POOL);
List<String> options = new ArrayList<>();
options.add(SUBMIT_SCORE);
if (configurationService.getBooleanProperty("workflow.reviewer.file-edit", false)) {
options.add(SUBMIT_EDIT_METADATA);
}
options.add(RETURN_TO_POOL);
return options;
}
@Override

View File

@@ -21,6 +21,8 @@ import org.dspace.content.WorkspaceItem;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.dspace.workflow.WorkflowException;
import org.dspace.xmlworkflow.factory.XmlWorkflowServiceFactory;
import org.dspace.xmlworkflow.state.Step;
@@ -40,6 +42,9 @@ import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem;
public class SingleUserReviewAction extends ProcessingAction {
private static final Logger log = LogManager.getLogger(SingleUserReviewAction.class);
private final ConfigurationService configurationService
= DSpaceServicesFactory.getInstance().getConfigurationService();
public static final int OUTCOME_REJECT = 1;
protected static final String SUBMIT_DECLINE_TASK = "submit_decline_task";
@@ -95,6 +100,9 @@ public class SingleUserReviewAction extends ProcessingAction {
public List<String> getOptions() {
List<String> options = new ArrayList<>();
options.add(SUBMIT_APPROVE);
if (configurationService.getBooleanProperty("workflow.reviewer.file-edit", false)) {
options.add(SUBMIT_EDIT_METADATA);
}
options.add(SUBMIT_REJECT);
options.add(SUBMIT_DECLINE_TASK);
return options;

View File

@@ -13,6 +13,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.apache.commons.collections4.CollectionUtils;
@@ -100,12 +101,17 @@ public class PoolTaskServiceImpl implements PoolTaskService {
//If the user does not have a claimedtask yet, see whether one of the groups of the user has pooltasks
//for this workflow item
Set<Group> groups = groupService.allMemberGroupsSet(context, ePerson);
for (Group group : groups) {
poolTask = poolTaskDAO.findByWorkflowItemAndGroup(context, group, workflowItem);
if (poolTask != null) {
return poolTask;
}
List<PoolTask> generalTasks = poolTaskDAO.findByWorkflowItem(context, workflowItem);
Optional<PoolTask> firstClaimedTask = groups.stream()
.flatMap(group -> generalTasks.stream()
.filter(f -> f.getGroup().getID().equals(group.getID()))
.findFirst()
.stream())
.findFirst();
if (firstClaimedTask.isPresent()) {
return firstClaimedTask.get();
}
}
}

View File

@@ -0,0 +1,21 @@
--
-- 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/
--
-- In the workspaceitem table, if there are multiple rows referring to the same item ID, keep only the first of them.
DELETE FROM workspaceitem WHERE EXISTS (
SELECT item_id
FROM workspaceitem
GROUP BY item_id
HAVING COUNT(workspace_item_id) > 1
) AND workspaceitem.workspace_item_id NOT IN (
SELECT MIN(workspace_item_id) AS workspace_item_id
FROM workspaceitem
GROUP BY item_id
);
-- Identify which rows have duplicates, and compute their replacements.
ALTER TABLE workspaceitem ADD CONSTRAINT unique_item_id UNIQUE(item_id);

View File

@@ -0,0 +1,21 @@
--
-- 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/
--
-- In the workspaceitem table, if there are multiple rows referring to the same item ID, keep only the first of them.
WITH dedup AS (
SELECT item_id, MIN(workspace_item_id) AS workspace_item_id
FROM workspaceitem
GROUP BY item_id
HAVING COUNT(workspace_item_id) > 1
)
DELETE FROM workspaceitem
USING dedup
WHERE workspaceitem.item_id = dedup.item_id AND workspaceitem.workspace_item_id <> dedup.workspace_item_id;
-- Enforce uniqueness of item_id in workspaceitem table.
ALTER TABLE workspaceitem ADD CONSTRAINT unique_item_id UNIQUE(item_id);

View File

@@ -51,11 +51,21 @@
<bean id="DataCiteImportService"
class="org.dspace.importer.external.datacite.DataCiteImportMetadataSourceServiceImpl" scope="singleton">
<property name="metadataFieldMapping" ref="DataCiteMetadataFieldMapping"/>
<property name="entityFilterQuery" value="${datacite.publicationimport.entityfilterquery}" />
</bean>
<bean id="DataCiteMetadataFieldMapping"
class="org.dspace.importer.external.datacite.DataCiteFieldMapping">
</bean>
<bean id="DataCiteProjectImportService"
class="org.dspace.importer.external.datacite.DataCiteProjectImportMetadataSourceServiceImpl" scope="singleton">
<property name="metadataFieldMapping" ref="DataCiteProjectMetadataFieldMapping"/>
<property name="entityFilterQuery" value="${datacite.projectimport.entityfilterquery}" />
</bean>
<bean id="DataCiteProjectMetadataFieldMapping"
class="org.dspace.importer.external.datacite.DataCiteProjectFieldMapping">
</bean>
<bean id="ArXivImportService"
class="org.dspace.importer.external.arxiv.service.ArXivImportMetadataSourceServiceImpl" scope="singleton">
<property name="metadataFieldMapping" ref="ArXivMetadataFieldMapping"/>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<node id='Countries' label='Countries'>
<isComposedBy>
<node id='Africa' label='Africa'>
<isComposedBy>
<node id='DZA' label='Algeria'/>
</isComposedBy>
</node>
</isComposedBy>
</node>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<node id='Countries' label='Länder'>
<isComposedBy>
<node id='Africa' label='Afrika'>
<isComposedBy>
<node id='DZA' label='Algerien'/>
</isComposedBy>
</node>
</isComposedBy>
</node>

View File

@@ -156,8 +156,6 @@ useProxies = true
proxies.trusted.ipranges = 7.7.7.7
proxies.trusted.include_ui_ip = true
csvexport.dir = dspace-server-webapp/src/test/data/dspaceFolder/exports
# For the tests we have to disable this health indicator because there isn't a mock server and the calculated status was DOWN
management.health.solrOai.enabled = false
@@ -175,6 +173,9 @@ authority.controlled.dspace.object.owner = true
webui.browse.link.1 = author:dc.contributor.*
webui.browse.link.2 = subject:dc.subject.*
# Configuration required for testing the controlled vocabulary functionality, which is configured using properties
vocabulary.plugin.countries.hierarchy.store=false
vocabulary.plugin.countries.storeIDs=true
# Enable duplicate detection for tests
duplicate.enable = true

View File

@@ -104,5 +104,16 @@
</list>
</property>
</bean>
<bean id="dataciteProjectLiveImportDataProvider" class="org.dspace.external.provider.impl.LiveImportDataProvider">
<property name="metadataSource" ref="DataCiteProjectImportService"/>
<property name="sourceIdentifier" value="dataciteProject"/>
<property name="recordIdMetadata" value="dc.identifier"/>
<property name="supportedEntityTypes">
<list>
<value>Project</value>
</list>
</property>
</bean>
</beans>

View File

@@ -21,8 +21,12 @@ import org.dspace.builder.AbstractBuilder;
import org.dspace.discovery.SearchUtils;
import org.dspace.servicemanager.DSpaceKernelImpl;
import org.dspace.servicemanager.DSpaceKernelInit;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.rules.TestName;
/**
* Abstract Test class copied from DSpace API
@@ -46,6 +50,12 @@ public class AbstractDSpaceIntegrationTest {
*/
protected static DSpaceKernelImpl kernelImpl;
/**
* Obtain the TestName from JUnit, so that we can print it out in the test logs (see below)
*/
@Rule
public TestName testName = new TestName();
/**
* Default constructor
*/
@@ -90,6 +100,20 @@ public class AbstractDSpaceIntegrationTest {
}
}
@Before
public void printTestMethodBefore() {
// Log the test method being executed. Put lines around it to make it stand out.
log.info("---");
log.info("Starting execution of test method: {}()", testName.getMethodName());
log.info("---");
}
@After
public void printTestMethodAfter() {
// Log the test method just completed.
log.info("Finished execution of test method: {}()", testName.getMethodName());
}
/**
* This method will be run after all tests finish as per @AfterClass. It
* will clean resources initialized by the @BeforeClass methods.

View File

@@ -18,9 +18,13 @@ import java.util.TimeZone;
import org.apache.logging.log4j.Logger;
import org.dspace.servicemanager.DSpaceKernelImpl;
import org.dspace.servicemanager.DSpaceKernelInit;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
@@ -62,6 +66,12 @@ public class AbstractDSpaceTest {
*/
protected static DSpaceKernelImpl kernelImpl;
/**
* Obtain the TestName from JUnit, so that we can print it out in the test logs (see below)
*/
@Rule
public TestName testName = new TestName();
/**
* This method will be run before the first test as per @BeforeClass. It will
* initialize shared resources required for all tests of this class.
@@ -94,6 +104,19 @@ public class AbstractDSpaceTest {
}
}
@Before
public void printTestMethodBefore() {
// Log the test method being executed. Put lines around it to make it stand out.
log.info("---");
log.info("Starting execution of test method: {}()", testName.getMethodName());
log.info("---");
}
@After
public void printTestMethodAfter() {
// Log the test method just completed.
log.info("Finished execution of test method: {}()", testName.getMethodName());
}
/**
* This method will be run after all tests finish as per @AfterClass. It

View File

@@ -20,8 +20,8 @@ import org.dspace.app.launcher.ScriptLauncher;
import org.dspace.app.scripts.handler.impl.TestDSpaceRunnableHandler;
import org.dspace.authority.AuthoritySearchService;
import org.dspace.authority.MockAuthoritySolrServiceImpl;
import org.dspace.authorize.AuthorizeException;
import org.dspace.builder.AbstractBuilder;
import org.dspace.builder.EPersonBuilder;
import org.dspace.content.Community;
import org.dspace.core.Context;
import org.dspace.core.I18nUtil;
@@ -127,19 +127,16 @@ public class AbstractIntegrationTestWithDatabase extends AbstractDSpaceIntegrati
EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
eperson = ePersonService.findByEmail(context, "test@email.com");
if (eperson == null) {
// This EPerson creation should only happen once (i.e. for first test run)
log.info("Creating initial EPerson (email=test@email.com) for Unit Tests");
eperson = ePersonService.create(context);
eperson.setFirstName(context, "first");
eperson.setLastName(context, "last");
eperson.setEmail("test@email.com");
eperson.setCanLogIn(true);
eperson.setLanguage(context, I18nUtil.getDefaultLocale().getLanguage());
ePersonService.setPassword(eperson, password);
// actually save the eperson to unit testing DB
ePersonService.update(context, eperson);
// Create test EPerson for usage in all tests
log.info("Creating Test EPerson (email=test@email.com) for Integration Tests");
eperson = EPersonBuilder.createEPerson(context)
.withNameInMetadata("first", "last")
.withEmail("test@email.com")
.withCanLogin(true)
.withLanguage(I18nUtil.getDefaultLocale().getLanguage())
.withPassword(password)
.build();
}
// Set our global test EPerson as the current user in DSpace
context.setCurrentUser(eperson);
@@ -148,26 +145,23 @@ public class AbstractIntegrationTestWithDatabase extends AbstractDSpaceIntegrati
admin = ePersonService.findByEmail(context, "admin@email.com");
if (admin == null) {
// This EPerson creation should only happen once (i.e. for first test run)
log.info("Creating initial EPerson (email=admin@email.com) for Unit Tests");
admin = ePersonService.create(context);
admin.setFirstName(context, "first (admin)");
admin.setLastName(context, "last (admin)");
admin.setEmail("admin@email.com");
admin.setCanLogIn(true);
admin.setLanguage(context, I18nUtil.getDefaultLocale().getLanguage());
ePersonService.setPassword(admin, password);
// actually save the eperson to unit testing DB
ePersonService.update(context, admin);
// Create test Administrator for usage in all tests
log.info("Creating Test Admin EPerson (email=admin@email.com) for Integration Tests");
admin = EPersonBuilder.createEPerson(context)
.withNameInMetadata("first (admin)", "last (admin)")
.withEmail("admin@email.com")
.withCanLogin(true)
.withLanguage(I18nUtil.getDefaultLocale().getLanguage())
.withPassword(password)
.build();
// Add Test Administrator to the ADMIN group in test database
GroupService groupService = EPersonServiceFactory.getInstance().getGroupService();
Group adminGroup = groupService.findByName(context, Group.ADMIN);
groupService.addMember(context, adminGroup, admin);
}
context.restoreAuthSystemState();
} catch (AuthorizeException ex) {
log.error("Error creating initial eperson or default groups", ex);
fail("Error creating initial eperson or default groups in AbstractUnitTest init()");
} catch (SQLException ex) {
log.error(ex.getMessage(), ex);
fail("SQL Error on AbstractUnitTest init()");

View File

@@ -23,7 +23,8 @@ import java.util.List;
import com.google.common.io.Files;
import com.opencsv.CSVReader;
import com.opencsv.exceptions.CsvException;
import org.apache.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.AbstractIntegrationTestWithDatabase;
import org.dspace.app.launcher.ScriptLauncher;
import org.dspace.app.scripts.handler.impl.TestDSpaceRunnableHandler;
@@ -51,7 +52,7 @@ public class MetadataExportSearchIT extends AbstractIntegrationTestWithDatabase
private Item[] itemsSubject2 = new Item[numberItemsSubject2];
private String filename;
private Collection collection;
private Logger logger = Logger.getLogger(MetadataExportSearchIT.class);
private Logger logger = LogManager.getLogger(MetadataExportSearchIT.class);
private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
private SearchService searchService;

View File

@@ -0,0 +1,511 @@
/**
* 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.authenticate;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import org.dspace.AbstractUnitTest;
import org.dspace.builder.AbstractBuilder;
import org.dspace.builder.EPersonBuilder;
import org.dspace.content.MetadataValue;
import org.dspace.eperson.EPerson;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
public class SamlAuthenticationTest extends AbstractUnitTest {
private static ConfigurationService configurationService;
private HttpServletRequest request;
private SamlAuthentication samlAuth;
private EPerson testUser;
@BeforeClass
public static void beforeAll() {
configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
AbstractBuilder.init(); // AbstractUnitTest doesn't do this for us.
}
@Before
public void beforeEach() throws Exception {
configurationService.setProperty("authentication-saml.autoregister", true);
configurationService.setProperty("authentication-saml.eperson.metadata.autocreate", true);
request = new MockHttpServletRequest();
samlAuth = new SamlAuthentication();
testUser = null;
}
@After
public void afterEach() throws Exception {
if (testUser != null) {
EPersonBuilder.deleteEPerson(testUser.getID());
}
}
@AfterClass
public static void afterAll() {
AbstractBuilder.destroy(); // AbstractUnitTest doesn't do this for us.
}
@Test
public void testAuthenticateExistingUserByEmail() throws Exception {
context.setCurrentUser(null);
context.turnOffAuthorisationSystem();
testUser = EPersonBuilder.createEPerson(context)
.withEmail("alyssa@dspace.org")
.withNameInMetadata("Alyssa", "Hacker")
.withCanLogin(true)
.build();
context.restoreAuthSystemState();
request.setAttribute("org.dspace.saml.EMAIL", List.of("alyssa@dspace.org"));
int result = samlAuth.authenticate(context, null, null, null, request);
assertEquals(AuthenticationMethod.SUCCESS, result);
EPerson user = context.getCurrentUser();
assertNotNull(user);
assertEquals("alyssa@dspace.org", user.getEmail());
assertNull(user.getNetid());
assertEquals("Alyssa", user.getFirstName());
assertEquals("Hacker", user.getLastName());
}
@Test
public void testAuthenticateExistingUserByNetId() throws Exception {
context.setCurrentUser(null);
context.turnOffAuthorisationSystem();
testUser = EPersonBuilder.createEPerson(context)
.withEmail("alyssa@dspace.org")
.withNetId("001")
.withNameInMetadata("Alyssa", "Hacker")
.withCanLogin(true)
.build();
context.restoreAuthSystemState();
request.setAttribute("org.dspace.saml.NAME_ID", "001");
int result = samlAuth.authenticate(context, null, null, null, request);
assertEquals(AuthenticationMethod.SUCCESS, result);
EPerson user = context.getCurrentUser();
assertNotNull(user);
assertEquals("alyssa@dspace.org", user.getEmail());
assertEquals("001", user.getNetid());
assertEquals("Alyssa", user.getFirstName());
assertEquals("Hacker", user.getLastName());
}
@Test
public void testAuthenticateExistingUserByEmailWithUnexpectedNetId() throws Exception {
EPerson originalUser = context.getCurrentUser();
context.turnOffAuthorisationSystem();
testUser = EPersonBuilder.createEPerson(context)
.withEmail("ben@dspace.org")
.withNetId("002")
.withNameInMetadata("Ben", "Bitdiddle")
.withCanLogin(true)
.build();
context.restoreAuthSystemState();
request.setAttribute("org.dspace.saml.EMAIL", List.of("ben@dspace.org"));
request.setAttribute("org.dspace.saml.NAME_ID", "oh-no-its-different-than-the-stored-netid");
int result = samlAuth.authenticate(context, null, null, null, request);
assertEquals(AuthenticationMethod.NO_SUCH_USER, result);
assertEquals(originalUser, context.getCurrentUser());
}
@Test
public void testAuthenticateExistingUserByEmailUpdatesNullNetId() throws Exception {
context.setCurrentUser(null);
context.turnOffAuthorisationSystem();
testUser = EPersonBuilder.createEPerson(context)
.withEmail("carrie@dspace.org")
.withNameInMetadata("Carrie", "Pragma")
.withCanLogin(true)
.build();
context.restoreAuthSystemState();
request.setAttribute("org.dspace.saml.EMAIL", List.of("carrie@dspace.org"));
request.setAttribute("org.dspace.saml.NAME_ID", "netid-from-idp");
int result = samlAuth.authenticate(context, null, null, null, request);
assertEquals(AuthenticationMethod.SUCCESS, result);
EPerson user = context.getCurrentUser();
assertNotNull(user);
assertEquals("carrie@dspace.org", user.getEmail());
assertEquals("netid-from-idp", user.getNetid());
assertEquals("Carrie", user.getFirstName());
assertEquals("Pragma", user.getLastName());
}
@Test
public void testAuthenticateExistingUserByNetIdUpdatesEmail() throws Exception {
context.setCurrentUser(null);
context.turnOffAuthorisationSystem();
testUser = EPersonBuilder.createEPerson(context)
.withEmail("alyssa@dspace.org")
.withNetId("001")
.withNameInMetadata("Alyssa", "Hacker")
.withCanLogin(true)
.build();
context.restoreAuthSystemState();
request.setAttribute("org.dspace.saml.NAME_ID", "001");
request.setAttribute("org.dspace.saml.EMAIL", List.of("aphacker@dspace.org"));
int result = samlAuth.authenticate(context, null, null, null, request);
assertEquals(AuthenticationMethod.SUCCESS, result);
EPerson user = context.getCurrentUser();
assertNotNull(user);
assertEquals("aphacker@dspace.org", user.getEmail());
assertEquals("001", user.getNetid());
assertEquals("Alyssa", user.getFirstName());
assertEquals("Hacker", user.getLastName());
}
@Test
public void testAuthenticateExistingUserUpdatesName() throws Exception {
context.setCurrentUser(null);
context.turnOffAuthorisationSystem();
testUser = EPersonBuilder.createEPerson(context)
.withEmail("alyssa@dspace.org")
.withNetId("001")
.withNameInMetadata("Alyssa", "Hacker")
.withCanLogin(true)
.build();
context.restoreAuthSystemState();
request.setAttribute("org.dspace.saml.NAME_ID", "001");
request.setAttribute("org.dspace.saml.GIVEN_NAME", "Liz");
request.setAttribute("org.dspace.saml.SURNAME", "Hacker-Bitdiddle");
int result = samlAuth.authenticate(context, null, null, null, request);
assertEquals(AuthenticationMethod.SUCCESS, result);
EPerson user = context.getCurrentUser();
assertNotNull(user);
assertEquals("alyssa@dspace.org", user.getEmail());
assertEquals("001", user.getNetid());
assertEquals("Liz", user.getFirstName());
assertEquals("Hacker-Bitdiddle", user.getLastName());
}
@Test
public void testAuthenticateExistingUserAdditionalMetadata() throws Exception {
configurationService.setProperty("authentication-saml.eperson.metadata",
"org.dspace.saml.PHONE => phone," +
"org.dspace.saml.NICKNAME => nickname");
context.setCurrentUser(null);
context.turnOffAuthorisationSystem();
testUser = EPersonBuilder.createEPerson(context)
.withEmail("alyssa@dspace.org")
.withNetId("001")
.withNameInMetadata("Alyssa", "Hacker")
.withCanLogin(true)
.build();
context.restoreAuthSystemState();
request.setAttribute("org.dspace.saml.NAME_ID", "001");
request.setAttribute("org.dspace.saml.PHONE", "123-456-7890");
request.setAttribute("org.dspace.saml.NICKNAME", "Liz");
int result = samlAuth.authenticate(context, null, null, null, request);
assertEquals(AuthenticationMethod.SUCCESS, result);
EPerson user = context.getCurrentUser();
assertNotNull(user);
assertEquals("alyssa@dspace.org", user.getEmail());
assertEquals("001", user.getNetid());
assertEquals("Alyssa", user.getFirstName());
assertEquals("Hacker", user.getLastName());
List<MetadataValue> metadata = user.getMetadata();
assertEquals(4, metadata.size());
assertEquals("eperson_phone", metadata.get(2).getMetadataField().toString());
assertEquals("123-456-7890", metadata.get(2).getValue());
assertEquals("eperson_nickname", metadata.get(3).getMetadataField().toString());
assertEquals("Liz", metadata.get(3).getValue());
}
@Test
public void testInvalidAdditionalMetadataMappingsAreIgnored() throws Exception {
configurationService.setProperty("authentication-saml.eperson.metadata",
"oops this is bad," +
"org.dspace.saml.NICKNAME => nickname");
context.setCurrentUser(null);
context.turnOffAuthorisationSystem();
testUser = EPersonBuilder.createEPerson(context)
.withEmail("alyssa@dspace.org")
.withNetId("001")
.withNameInMetadata("Alyssa", "Hacker")
.withCanLogin(true)
.build();
context.restoreAuthSystemState();
request.setAttribute("org.dspace.saml.NAME_ID", "001");
request.setAttribute("org.dspace.saml.PHONE", "123-456-7890");
request.setAttribute("org.dspace.saml.NICKNAME", "Liz");
int result = samlAuth.authenticate(context, null, null, null, request);
assertEquals(AuthenticationMethod.SUCCESS, result);
EPerson user = context.getCurrentUser();
assertNotNull(user);
assertEquals("alyssa@dspace.org", user.getEmail());
assertEquals("001", user.getNetid());
assertEquals("Alyssa", user.getFirstName());
assertEquals("Hacker", user.getLastName());
List<MetadataValue> metadata = user.getMetadata();
assertEquals(3, metadata.size());
assertEquals("eperson_nickname", metadata.get(2).getMetadataField().toString());
assertEquals("Liz", metadata.get(2).getValue());
}
@Test
public void testAuthenticateExistingUserAdditionalMetadataAutocreateDisabled() throws Exception {
configurationService.setProperty("authentication-saml.eperson.metadata.autocreate", false);
configurationService.setProperty("authentication-saml.eperson.metadata",
"org.dspace.saml.PHONE => phone," +
"org.dspace.saml.DEPARTMENT => department");
context.setCurrentUser(null);
context.turnOffAuthorisationSystem();
testUser = EPersonBuilder.createEPerson(context)
.withEmail("alyssa@dspace.org")
.withNetId("001")
.withNameInMetadata("Alyssa", "Hacker")
.withCanLogin(true)
.build();
context.restoreAuthSystemState();
request.setAttribute("org.dspace.saml.NAME_ID", "001");
request.setAttribute("org.dspace.saml.PHONE", "123-456-7890");
request.setAttribute("org.dspace.saml.DEPARTMENT", "Library");
int result = samlAuth.authenticate(context, null, null, null, request);
assertEquals(AuthenticationMethod.SUCCESS, result);
EPerson user = context.getCurrentUser();
assertNotNull(user);
assertEquals("alyssa@dspace.org", user.getEmail());
assertEquals("001", user.getNetid());
assertEquals("Alyssa", user.getFirstName());
assertEquals("Hacker", user.getLastName());
List<MetadataValue> metadata = user.getMetadata();
assertEquals(3, metadata.size());
assertEquals("eperson_phone", metadata.get(2).getMetadataField().toString());
assertEquals("123-456-7890", metadata.get(2).getValue());
}
@Test
public void testAdditionalMetadataWithInvalidNameNotAutocreated() throws Exception {
configurationService.setProperty("authentication-saml.eperson.metadata",
"org.dspace.saml.PHONE => phone," +
"org.dspace.saml.DEPARTMENT => (department)"); // parens not allowed
context.setCurrentUser(null);
context.turnOffAuthorisationSystem();
testUser = EPersonBuilder.createEPerson(context)
.withEmail("alyssa@dspace.org")
.withNetId("001")
.withNameInMetadata("Alyssa", "Hacker")
.withCanLogin(true)
.build();
context.restoreAuthSystemState();
request.setAttribute("org.dspace.saml.NAME_ID", "001");
request.setAttribute("org.dspace.saml.PHONE", "123-456-7890");
request.setAttribute("org.dspace.saml.DEPARTMENT", "Library");
int result = samlAuth.authenticate(context, null, null, null, request);
assertEquals(AuthenticationMethod.SUCCESS, result);
EPerson user = context.getCurrentUser();
assertNotNull(user);
assertEquals("alyssa@dspace.org", user.getEmail());
assertEquals("001", user.getNetid());
assertEquals("Alyssa", user.getFirstName());
assertEquals("Hacker", user.getLastName());
List<MetadataValue> metadata = user.getMetadata();
assertEquals(3, metadata.size());
assertEquals("eperson_phone", metadata.get(2).getMetadataField().toString());
assertEquals("123-456-7890", metadata.get(2).getValue());
}
@Test
public void testExistingUserLoginDisabled() throws Exception {
EPerson originalUser = context.getCurrentUser();
context.turnOffAuthorisationSystem();
testUser = EPersonBuilder.createEPerson(context)
.withEmail("alyssa@dspace.org")
.withNameInMetadata("Alyssa", "Hacker")
.withCanLogin(false)
.build();
context.restoreAuthSystemState();
request.setAttribute("org.dspace.saml.EMAIL", List.of("alyssa@dspace.org"));
int result = samlAuth.authenticate(context, null, null, null, request);
assertEquals(AuthenticationMethod.BAD_ARGS, result);
assertEquals(originalUser, context.getCurrentUser());
}
@Test
public void testNonExistentUserWithoutEmail() throws Exception {
EPerson originalUser = context.getCurrentUser();
request.setAttribute("org.dspace.saml.NAME_ID", "non-existent-netid");
int result = samlAuth.authenticate(context, null, null, null, request);
assertEquals(AuthenticationMethod.NO_SUCH_USER, result);
assertEquals(originalUser, context.getCurrentUser());
}
@Test
public void testNonExistentUserWithEmailAutoregisterEnabled() throws Exception {
context.setCurrentUser(null);
request.setAttribute("org.dspace.saml.NAME_ID", "non-existent-netid");
request.setAttribute("org.dspace.saml.EMAIL", List.of("ben@dspace.org"));
request.setAttribute("org.dspace.saml.GIVEN_NAME", "Ben");
request.setAttribute("org.dspace.saml.SURNAME", "Bitdiddle");
int result = samlAuth.authenticate(context, null, null, null, request);
assertEquals(AuthenticationMethod.SUCCESS, result);
EPerson user = context.getCurrentUser();
assertNotNull(user);
assertEquals("ben@dspace.org", user.getEmail());
assertEquals("non-existent-netid", user.getNetid());
assertEquals("Ben", user.getFirstName());
assertEquals("Bitdiddle", user.getLastName());
assertTrue(user.canLogIn());
assertTrue(user.getSelfRegistered());
testUser = user; // Make sure the autoregistered user gets deleted.
}
@Test
public void testNonExistentUserWithEmailAutoregisterDisabled() throws Exception {
configurationService.setProperty("authentication-saml.autoregister", false);
EPerson originalUser = context.getCurrentUser();
request.setAttribute("org.dspace.saml.NAME_ID", "non-existent-netid");
request.setAttribute("org.dspace.saml.EMAIL", List.of("ben@dspace.org"));
request.setAttribute("org.dspace.saml.GIVEN_NAME", "Ben");
request.setAttribute("org.dspace.saml.SURNAME", "Bitdiddle");
int result = samlAuth.authenticate(context, null, null, null, request);
assertEquals(AuthenticationMethod.NO_SUCH_USER, result);
assertEquals(originalUser, context.getCurrentUser());
}
@Test
public void testNoEmailOrNameIdInRequest() throws Exception {
context.setCurrentUser(null);
context.turnOffAuthorisationSystem();
testUser = EPersonBuilder.createEPerson(context)
.withEmail("alyssa@dspace.org")
.withNameInMetadata("Alyssa", "Hacker")
.withCanLogin(true)
.build();
context.restoreAuthSystemState();
int result = samlAuth.authenticate(context, null, null, null, request);
assertEquals(AuthenticationMethod.NO_SUCH_USER, result);
}
@Test
public void testRequestIsNull() throws Exception {
EPerson originalUser = context.getCurrentUser();
int result = samlAuth.authenticate(context, null, null, null, null);
assertEquals(AuthenticationMethod.BAD_ARGS, result);
assertEquals(originalUser, context.getCurrentUser());
}
}

View File

@@ -11,7 +11,8 @@ import java.io.IOException;
import java.sql.SQLException;
import java.util.Date;
import org.apache.log4j.Logger;
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.orcid.OrcidHistory;
@@ -24,7 +25,7 @@ import org.dspace.orcid.service.OrcidHistoryService;
*/
public class OrcidHistoryBuilder extends AbstractBuilder<OrcidHistory, OrcidHistoryService> {
private static final Logger log = Logger.getLogger(OrcidHistoryBuilder.class);
private static final Logger log = LogManager.getLogger(OrcidHistoryBuilder.class);
private OrcidHistory orcidHistory;

View File

@@ -59,7 +59,7 @@ import org.dspace.content.virtual.Collected;
import org.dspace.content.virtual.VirtualMetadataConfiguration;
import org.dspace.content.virtual.VirtualMetadataPopulator;
import org.dspace.core.Constants;
import org.dspace.discovery.SolrSearchCore;
import org.dspace.discovery.MockSolrSearchCore;
import org.dspace.kernel.ServiceManager;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.dspace.versioning.Version;
@@ -79,8 +79,9 @@ public class VersioningWithRelationshipsIT extends AbstractIntegrationTestWithDa
ContentServiceFactory.getInstance().getInstallItemService();
private final ItemService itemService =
ContentServiceFactory.getInstance().getItemService();
private final SolrSearchCore solrSearchCore =
DSpaceServicesFactory.getInstance().getServiceManager().getServicesByType(SolrSearchCore.class).get(0);
private final MockSolrSearchCore solrSearchCore =
DSpaceServicesFactory.getInstance().getServiceManager().getServiceByName(null, MockSolrSearchCore.class);
protected Community community;
protected Collection collection;
protected EntityType publicationEntityType;

View File

@@ -12,6 +12,7 @@ import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
@@ -39,6 +40,7 @@ import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.factory.EPersonServiceFactory;
import org.dspace.eperson.service.EPersonService;
import org.dspace.workflow.MockWorkflowItem;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -468,4 +470,14 @@ public class WorkspaceItemTest extends AbstractUnitTest {
assertTrue("testSetPublishedBefore 0", wi.isPublishedBefore());
}
@Test
public void testDuplicateItemID() throws Exception {
context.turnOffAuthorisationSystem();
Item item = wi.getItem();
MockWorkflowItem wfItem = new MockWorkflowItem();
wfItem.item = item;
wfItem.collection = collection;
assertThrows(IllegalArgumentException.class, () -> workspaceItemService.create(context, wfItem));
context.restoreAuthSystemState();
}
}

View File

@@ -89,6 +89,145 @@ public class DSpaceControlledVocabularyTest extends AbstractDSpaceTest {
assertEquals("north 40", result.values[0].value);
}
/**
* Test of getMatches method of class
* DSpaceControlledVocabulary using a localized controlled vocabulary with no locale (fallback to default)
* @throws java.lang.ClassNotFoundException passed through.
*/
@Test
public void testGetMatchesNoLocale() throws ClassNotFoundException {
final String PLUGIN_INTERFACE = "org.dspace.content.authority.ChoiceAuthority";
String idValue = "DZA";
String labelPart = "Alge";
int start = 0;
int limit = 10;
// This "countries" Controlled Vocab is included in TestEnvironment data
// (under /src/test/data/dspaceFolder/) and it should be auto-loaded
// by test configs in /src/test/data/dspaceFolder/config/local.cfg
DSpaceControlledVocabulary instance = (DSpaceControlledVocabulary)
CoreServiceFactory.getInstance().getPluginService().getNamedPlugin(Class.forName(PLUGIN_INTERFACE),
"countries");
assertNotNull(instance);
Choices result = instance.getMatches(labelPart, start, limit, null);
assertEquals(idValue, result.values[0].value);
assertEquals("Algeria", result.values[0].label);
}
/**
* Test of getBestMatch method of class
* DSpaceControlledVocabulary using a localized controlled vocabulary with no locale (fallback to default)
* @throws java.lang.ClassNotFoundException passed through.
*/
@Test
public void testGetBestMatchIdValueNoLocale() throws ClassNotFoundException {
final String PLUGIN_INTERFACE = "org.dspace.content.authority.ChoiceAuthority";
String idValue = "DZA";
// This "countries" Controlled Vocab is included in TestEnvironment data
// (under /src/test/data/dspaceFolder/) and it should be auto-loaded
// by test configs in /src/test/data/dspaceFolder/config/local.cfg
DSpaceControlledVocabulary instance = (DSpaceControlledVocabulary)
CoreServiceFactory.getInstance().getPluginService().getNamedPlugin(Class.forName(PLUGIN_INTERFACE),
"countries");
assertNotNull(instance);
Choices result = instance.getBestMatch(idValue, null);
assertEquals(idValue, result.values[0].value);
assertEquals("Algeria", result.values[0].label);
}
/**
* Test of getMatches method of class
* DSpaceControlledVocabulary using a localized controlled vocabulary with valid locale parameter (localized
* label returned)
*/
@Test
public void testGetMatchesGermanLocale() throws ClassNotFoundException {
final String PLUGIN_INTERFACE = "org.dspace.content.authority.ChoiceAuthority";
String idValue = "DZA";
String labelPart = "Alge";
int start = 0;
int limit = 10;
// This "countries" Controlled Vocab is included in TestEnvironment data
// (under /src/test/data/dspaceFolder/) and it should be auto-loaded
// by test configs in /src/test/data/dspaceFolder/config/local.cfg
DSpaceControlledVocabulary instance = (DSpaceControlledVocabulary)
CoreServiceFactory.getInstance().getPluginService().getNamedPlugin(Class.forName(PLUGIN_INTERFACE),
"countries");
assertNotNull(instance);
Choices result = instance.getMatches(labelPart, start, limit, "de");
assertEquals(idValue, result.values[0].value);
assertEquals("Algerien", result.values[0].label);
}
/**
* Test of getBestMatch method of class
* DSpaceControlledVocabulary using a localized controlled vocabulary with valid locale parameter (localized
* label returned)
*/
@Test
public void testGetBestMatchIdValueGermanLocale() throws ClassNotFoundException {
final String PLUGIN_INTERFACE = "org.dspace.content.authority.ChoiceAuthority";
String idValue = "DZA";
// This "countries" Controlled Vocab is included in TestEnvironment data
// (under /src/test/data/dspaceFolder/) and it should be auto-loaded
// by test configs in /src/test/data/dspaceFolder/config/local.cfg
DSpaceControlledVocabulary instance = (DSpaceControlledVocabulary)
CoreServiceFactory.getInstance().getPluginService().getNamedPlugin(Class.forName(PLUGIN_INTERFACE),
"countries");
assertNotNull(instance);
Choices result = instance.getBestMatch(idValue, "de");
assertEquals(idValue, result.values[0].value);
assertEquals("Algerien", result.values[0].label);
}
/**
* Test of getChoice method of class
* DSpaceControlledVocabulary using a localized controlled vocabulary with no locale (fallback to default)
* @throws java.lang.ClassNotFoundException passed through.
*/
@Test
public void testGetChoiceNoLocale() throws ClassNotFoundException {
final String PLUGIN_INTERFACE = "org.dspace.content.authority.ChoiceAuthority";
String idValue = "DZA";
// This "countries" Controlled Vocab is included in TestEnvironment data
// (under /src/test/data/dspaceFolder/) and it should be auto-loaded
// by test configs in /src/test/data/dspaceFolder/config/local.cfg
DSpaceControlledVocabulary instance = (DSpaceControlledVocabulary)
CoreServiceFactory.getInstance().getPluginService().getNamedPlugin(Class.forName(PLUGIN_INTERFACE),
"countries");
assertNotNull(instance);
Choice result = instance.getChoice(idValue, null);
assertEquals(idValue, result.value);
assertEquals("Algeria", result.label);
}
/**
* Test of getChoice method of class
* DSpaceControlledVocabulary using a localized controlled vocabulary with valid locale parameter (localized
* label returned)
* @throws java.lang.ClassNotFoundException passed through.
*/
@Test
public void testGetChoiceGermanLocale() throws ClassNotFoundException {
final String PLUGIN_INTERFACE = "org.dspace.content.authority.ChoiceAuthority";
String idValue = "DZA";
// This "countries" Controlled Vocab is included in TestEnvironment data
// (under /src/test/data/dspaceFolder/) and it should be auto-loaded
// by test configs in /src/test/data/dspaceFolder/config/local.cfg
DSpaceControlledVocabulary instance = (DSpaceControlledVocabulary)
CoreServiceFactory.getInstance().getPluginService().getNamedPlugin(Class.forName(PLUGIN_INTERFACE),
"countries");
assertNotNull(instance);
Choice result = instance.getChoice(idValue, "de");
assertEquals(idValue, result.value);
assertEquals("Algerien", result.label);
}
/**
* Test of getBestMatch method, of class DSpaceControlledVocabulary.
*/

View File

@@ -11,6 +11,7 @@ import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -990,4 +991,38 @@ public class ItemServiceIT extends AbstractIntegrationTestWithDatabase {
context.restoreAuthSystemState();
}
@Test
public void testIsLatestVersion() throws Exception {
assertTrue("Original should be the latest version", this.itemService.isLatestVersion(context, item));
context.turnOffAuthorisationSystem();
Version firstVersion = versioningService.createNewVersion(context, item);
Item firstPublication = firstVersion.getItem();
WorkspaceItem firstPublicationWSI = workspaceItemService.findByItem(context, firstPublication);
installItemService.installItem(context, firstPublicationWSI);
context.commit();
context.restoreAuthSystemState();
assertTrue("First version should be valid", this.itemService.isLatestVersion(context, firstPublication));
assertFalse("Original version should not be valid", this.itemService.isLatestVersion(context, item));
context.turnOffAuthorisationSystem();
Version secondVersion = versioningService.createNewVersion(context, item);
Item secondPublication = secondVersion.getItem();
WorkspaceItem secondPublicationWSI = workspaceItemService.findByItem(context, secondPublication);
installItemService.installItem(context, secondPublicationWSI);
context.commit();
context.restoreAuthSystemState();
assertTrue("Second version should be valid", this.itemService.isLatestVersion(context, secondPublication));
assertFalse("First version should not be valid", this.itemService.isLatestVersion(context, firstPublication));
assertFalse("Original version should not be valid", this.itemService.isLatestVersion(context, item));
context.turnOffAuthorisationSystem();
}
}

View File

@@ -558,4 +558,29 @@ public class ContextTest extends AbstractUnitTest {
cleanupContext(instance);
}
@Test
public void testUncacheEntities() throws Throwable {
// To set up the test, ensure the cache contains more than the current user entity
groupService.findByName(context, Group.ANONYMOUS);
assertTrue("Cache size should be greater than one", context.getDBConnection().getCacheSize() > 1);
context.uncacheEntities();
assertThat("Cache size should be one (current user)", context.getDBConnection().getCacheSize(), equalTo(1L));
context.reloadEntity(context.getCurrentUser());
assertThat("Cache should only contain the current user", context.getDBConnection().getCacheSize(), equalTo(1L));
}
@Test
public void testUncacheEntity() throws Throwable {
// Remember the cache size after loading an entity
Group group = groupService.findByName(context, Group.ANONYMOUS);
long oldCacheSize = context.getDBConnection().getCacheSize();
// Uncache the entity
context.uncacheEntity(group);
long newCacheSize = context.getDBConnection().getCacheSize();
assertThat("Cache size should be reduced by one", newCacheSize, equalTo(oldCacheSize - 1));
}
}

View File

@@ -205,6 +205,28 @@ public class HibernateDBConnectionTest extends AbstractUnitTest {
.contains(person));
}
/**
* Test of uncacheEntities method
*/
@Test
public void testUncacheEntities() throws SQLException {
// Get DBConnection associated with DSpace Context
HibernateDBConnection dbConnection = (HibernateDBConnection) context.getDBConnection();
EPerson person = context.getCurrentUser();
assertTrue("Current user should be cached in session", dbConnection.getSession()
.contains(person));
dbConnection.uncacheEntities();
assertFalse("Current user should be gone from cache", dbConnection.getSession()
.contains(person));
// Test ability to reload an uncached entity
person = dbConnection.reloadEntity(person);
assertTrue("Current user should be cached back in session", dbConnection.getSession()
.contains(person));
}
/**
* Test of uncacheEntity method
*/

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