mirror of
https://github.com/DSpace/DSpace.git
synced 2025-10-15 05:53:08 +00:00
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:
341
.github/dependabot.yml
vendored
Normal file
341
.github/dependabot.yml
vendored
Normal 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" ]
|
99
.github/workflows/docker.yml
vendored
99
.github/workflows/docker.yml
vendored
@@ -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
|
||||
|
225
.github/workflows/reusable-docker-build.yml
vendored
225
.github/workflows/reusable-docker-build.yml
vendored
@@ -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
4
.gitignore
vendored
@@ -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
|
||||
|
35
Dockerfile
35
Dockerfile
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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/*
|
||||
|
@@ -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"]
|
||||
|
@@ -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"/>
|
||||
|
@@ -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: .
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -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) {
|
||||
|
@@ -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;
|
||||
|
@@ -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) {
|
||||
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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.
|
||||
*
|
||||
|
@@ -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");
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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;
|
||||
|
@@ -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?
|
||||
*
|
||||
|
@@ -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++) {
|
||||
|
@@ -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());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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());
|
||||
|
@@ -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("'", "'").toLowerCase());
|
||||
xpathExpression +=
|
||||
String.format(xpathTemplate, textHierarchy[i].replaceAll("'", "'").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("'", "'"));
|
||||
xpathExpression +=
|
||||
String.format(valueTemplate, textHierarchy[i].replaceAll("'", "'"));
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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();
|
||||
|
@@ -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.
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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.disconnect();
|
||||
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
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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");
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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) {
|
||||
handler.logInfo("Removing " + commandLine.getOptionValue("r") + " from Index");
|
||||
indexer.unIndexContent(context, indexableObject.get().getUniqueIndexID());
|
||||
} else if (indexClientOptions == IndexClientOptions.CLEAN) {
|
||||
handler.logInfo("Cleaning Index");
|
||||
indexer.cleanIndex();
|
||||
} else if (indexClientOptions == IndexClientOptions.DELETE) {
|
||||
handler.logInfo("Deleting Index");
|
||||
indexer.deleteIndex();
|
||||
} else if (indexClientOptions == IndexClientOptions.BUILD ||
|
||||
indexClientOptions == IndexClientOptions.BUILDANDSPELLCHECK) {
|
||||
handler.logInfo("(Re)building index from scratch.");
|
||||
if (StringUtils.isNotBlank(type)) {
|
||||
handler.logWarning(String.format("Type option, %s, not applicable for entire index rebuild option, b" +
|
||||
", type will be ignored", TYPE_OPTION));
|
||||
}
|
||||
indexer.deleteIndex();
|
||||
indexer.createIndex(context);
|
||||
if (indexClientOptions == IndexClientOptions.BUILDANDSPELLCHECK) {
|
||||
switch (indexClientOptions) {
|
||||
case REMOVE:
|
||||
handler.logInfo("Removing " + commandLine.getOptionValue("r") + " from Index");
|
||||
indexer.unIndexContent(context, indexableObject.get().getUniqueIndexID());
|
||||
break;
|
||||
case CLEAN:
|
||||
handler.logInfo("Cleaning Index");
|
||||
indexer.cleanIndex();
|
||||
break;
|
||||
case DELETE:
|
||||
handler.logInfo("Deleting Index");
|
||||
indexer.deleteIndex();
|
||||
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));
|
||||
}
|
||||
indexer.deleteIndex();
|
||||
indexer.createIndex(context);
|
||||
if (indexClientOptions == IndexClientOptions.BUILDANDSPELLCHECK) {
|
||||
checkRebuildSpellCheck(commandLine, indexer);
|
||||
}
|
||||
break;
|
||||
case OPTIMIZE:
|
||||
handler.logInfo("Optimizing search core.");
|
||||
indexer.optimize();
|
||||
break;
|
||||
case SPELLCHECK:
|
||||
checkRebuildSpellCheck(commandLine, indexer);
|
||||
}
|
||||
} else if (indexClientOptions == IndexClientOptions.OPTIMIZE) {
|
||||
handler.logInfo("Optimizing search core.");
|
||||
indexer.optimize();
|
||||
} else if (indexClientOptions == IndexClientOptions.SPELLCHECK) {
|
||||
checkRebuildSpellCheck(commandLine, indexer);
|
||||
} else if (indexClientOptions == IndexClientOptions.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 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("Updating Index");
|
||||
indexer.updateIndex(context, false, type);
|
||||
if (indexClientOptions == IndexClientOptions.UPDATEANDSPELLCHECK) {
|
||||
checkRebuildSpellCheck(commandLine, indexer);
|
||||
}
|
||||
} else if (indexClientOptions == IndexClientOptions.FORCEUPDATE ||
|
||||
indexClientOptions == IndexClientOptions.FORCEUPDATEANDSPELLCHECK) {
|
||||
handler.logInfo("Updating Index");
|
||||
indexer.updateIndex(context, true, type);
|
||||
if (indexClientOptions == IndexClientOptions.FORCEUPDATEANDSPELLCHECK) {
|
||||
checkRebuildSpellCheck(commandLine, indexer);
|
||||
}
|
||||
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 seconds = (System.currentTimeMillis() - startTimeMillis) / 1000;
|
||||
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);
|
||||
}
|
||||
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");
|
||||
@@ -174,7 +150,7 @@ public class IndexClient extends DSpaceRunnable<IndexDiscoveryScriptConfiguratio
|
||||
@Override
|
||||
public IndexDiscoveryScriptConfiguration getScriptConfiguration() {
|
||||
return new DSpace().getServiceManager().getServiceByName("index-discovery",
|
||||
IndexDiscoveryScriptConfiguration.class);
|
||||
IndexDiscoveryScriptConfiguration.class);
|
||||
}
|
||||
|
||||
public void setup() throws ParseException {
|
||||
@@ -186,78 +162,93 @@ public class IndexClient extends DSpaceRunnable<IndexDiscoveryScriptConfiguratio
|
||||
}
|
||||
indexClientOptions = IndexClientOptions.getIndexClientOption(commandLine);
|
||||
}
|
||||
/**
|
||||
* Indexes the given object and all children, if applicable.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
private static long indexAll(final IndexingService indexingService,
|
||||
final ItemService itemService,
|
||||
final Context context,
|
||||
final IndexableObject dso)
|
||||
throws IOException, SearchServiceException, SQLException {
|
||||
long count = 0;
|
||||
|
||||
indexingService.indexContent(context, dso, true, true);
|
||||
count++;
|
||||
if (dso.getIndexedObject() instanceof Community) {
|
||||
final Community community = (Community) dso.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);
|
||||
}
|
||||
final Community reloadedCommunity = (Community) HandleServiceFactory.getInstance().getHandleService()
|
||||
.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
|
||||
context.uncacheEntity(collection);
|
||||
}
|
||||
} else if (dso instanceof IndexableCollection) {
|
||||
count += indexItems(indexingService, itemService, context, (Collection) dso.getIndexedObject());
|
||||
/**
|
||||
* Resolves the given parameter to an IndexableObject (Item, Collection, or Community).
|
||||
*
|
||||
* @param context The relevant DSpace Context.
|
||||
* @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 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.
|
||||
}
|
||||
|
||||
return count;
|
||||
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 all items in the given collection.
|
||||
* Indexes the given object and all its children recursively.
|
||||
*
|
||||
* @param indexingService
|
||||
* @param itemService
|
||||
* @param indexingService The indexing service.
|
||||
* @param itemService The item service.
|
||||
* @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.
|
||||
* @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 static long indexItems(final IndexingService indexingService,
|
||||
final ItemService itemService,
|
||||
final Context context,
|
||||
final Collection collection)
|
||||
throws IOException, SearchServiceException, SQLException {
|
||||
private long indexAll(final IndexingService indexingService, final ItemService itemService, final Context context,
|
||||
final IndexableObject indexableObject) throws IOException, SearchServiceException, SQLException {
|
||||
long count = 0;
|
||||
|
||||
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);
|
||||
boolean commit = indexableObject instanceof IndexableCommunity ||
|
||||
indexableObject instanceof IndexableCollection;
|
||||
indexingService.indexContent(context, indexableObject, true, commit);
|
||||
count++;
|
||||
|
||||
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));
|
||||
context.uncacheEntity(subcommunity);
|
||||
}
|
||||
// Reload community to get up-to-date collections
|
||||
final Community reloadedCommunity = (Community) HandleServiceFactory.getInstance().getHandleService()
|
||||
.resolveToObject(context, communityHandle);
|
||||
for (final Collection collection : reloadedCommunity.getCollections()) {
|
||||
count += indexAll(indexingService, itemService, context, new IndexableCollection(collection));
|
||||
context.uncacheEntity(collection);
|
||||
}
|
||||
} 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++;
|
||||
context.uncacheEntity(item);
|
||||
}
|
||||
indexingService.commit();
|
||||
}
|
||||
indexingService.commit();
|
||||
|
||||
return count;
|
||||
}
|
||||
@@ -268,10 +259,10 @@ 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 {
|
||||
throws SearchServiceException, IOException {
|
||||
handler.logInfo("Rebuilding spell checker.");
|
||||
indexer.buildSpellCheck();
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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));
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
* @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,28 +703,43 @@ 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());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
@@ -57,6 +57,11 @@ public class IdentifierServiceImpl implements IdentifierService {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<IdentifierProvider> getProviders() {
|
||||
return this.providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reserves identifiers for the item
|
||||
*
|
||||
|
@@ -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);
|
||||
itemService.addMetadata(c, item, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null, newIdentifiers);
|
||||
// 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);
|
||||
|
@@ -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) {
|
||||
organiser.reserve(doi);
|
||||
context.uncacheEntity(doi);
|
||||
doi = context.reloadEntity(doi);
|
||||
try {
|
||||
organiser.reserve(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) {
|
||||
organiser.register(doi);
|
||||
context.uncacheEntity(doi);
|
||||
doi = context.reloadEntity(doi);
|
||||
try {
|
||||
organiser.register(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();
|
||||
organiser.delete(doi.getDoi());
|
||||
context.uncacheEntity(doi);
|
||||
for (DOI doi : dois) {
|
||||
doi = context.reloadEntity(doi);
|
||||
try {
|
||||
organiser.delete(doi.getDoi());
|
||||
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;
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
@@ -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();
|
||||
}
|
||||
|
@@ -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.
|
||||
|
38
dspace-api/src/main/java/org/dspace/importer/external/datacite/DataCiteProjectFieldMapping.java
vendored
Normal file
38
dspace-api/src/main/java/org/dspace/importer/external/datacite/DataCiteProjectFieldMapping.java
vendored
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@@ -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";
|
||||
}
|
||||
}
|
@@ -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.
|
||||
*
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
|
||||
}
|
||||
|
@@ -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();
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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,17 +118,26 @@ public class OrcidQueueConsumer implements Consumer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (alreadyConsumedItems.contains(item.getID())) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.turnOffAuthorisationSystem();
|
||||
try {
|
||||
consumeItem(context, item);
|
||||
} finally {
|
||||
context.restoreAuthSystemState();
|
||||
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);
|
||||
} finally {
|
||||
context.restoreAuthSystemState();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
itemsToConsume.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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
|
||||
|
@@ -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.
|
||||
*
|
||||
|
@@ -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");
|
||||
|
@@ -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.
|
||||
*
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
clearSynchronizationSettings(context, profile);
|
||||
|
||||
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);
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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,12 +309,15 @@ 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");
|
||||
FileInputStream inputStream = FileUtils.openInputStream(tempFile);
|
||||
appendFile(context, process, inputStream, Process.OUTPUT_TYPE, process.getName() + process.getID() + ".log");
|
||||
inputStream.close();
|
||||
tempFile.delete();
|
||||
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.getID() + "-" + process.getName() + ".log");
|
||||
inputStream.close();
|
||||
tempFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@@ -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
|
||||
*/
|
||||
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
@@ -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);
|
@@ -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"/>
|
||||
|
@@ -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>
|
@@ -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>
|
@@ -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
|
||||
|
||||
|
@@ -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>
|
||||
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
|
@@ -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()");
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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.
|
||||
*/
|
||||
|
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
||||
|
@@ -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
Reference in New Issue
Block a user