diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a9ff8760e763..9d32cb119d41 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -15,6 +15,7 @@ on: permissions: contents: read # to fetch code (actions/checkout) + packages: write # to write images to GitHub Container Registry (GHCR) jobs: #################################################### @@ -147,4 +148,102 @@ jobs: tags_flavor: suffix=-loadsql secrets: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }} \ No newline at end of file + 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 diff --git a/.github/workflows/reusable-docker-build.yml b/.github/workflows/reusable-docker-build.yml index 7a8de661fa68..0c3261da95da 100644 --- a/.github/workflows/reusable-docker-build.yml +++ b/.github/workflows/reusable-docker-build.yml @@ -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) @@ -69,9 +72,12 @@ env: REDEPLOY_SANDBOX_URL: ${{ secrets.REDEPLOY_SANDBOX_URL }} REDEPLOY_DEMO_URL: ${{ secrets.REDEPLOY_DEMO_URL }} # Current DSpace branches (and architecture) which are deployed to demo.dspace.org & sandbox.dspace.org respectively - DEPLOY_DEMO_BRANCH: 'dspace-8_x' + DEPLOY_DEMO_BRANCH: 'dspace-9_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: @@ -80,22 +86,22 @@ jobs: matrix: # Architectures / Platforms for which we will build Docker images arch: [ 'linux/amd64', 'linux/arm64' ] - os: [ ubuntu-latest ] isPr: - ${{ github.event_name == 'pull_request' }} # If this is a PR, we ONLY build for AMD64. For PRs we only do a sanity check test to ensure Docker builds work. # The below exclude therefore ensures we do NOT build ARM64 for PRs. exclude: - isPr: true - os: ubuntu-latest arch: linux/arm64 - runs-on: ${{ matrix.os }} + # If ARM64, then use the Ubuntu ARM64 runner. Otherwise, use the Ubuntu AMD64 runner + runs-on: ${{ matrix.arch == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} steps: # 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 +111,41 @@ 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/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 }} + # 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: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_ACCESS_TOKEN }} + registry: ${{ env.DOCKER_BUILD_REGISTRY }} + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_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 +153,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 +174,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 +185,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 +252,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: | + 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: | - docker buildx imagetools inspect ${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} + curl -X POST $REDEPLOY_DEMO_URL \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2fcb46b9932c..529351edc5c2 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ tags .project .classpath .checkstyle +.factorypath ## Ignore project files created by IntelliJ IDEA *.iml diff --git a/Dockerfile b/Dockerfile index 964b76a2565d..eb90299ccd7d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,19 @@ # This image will be published as dspace/dspace # See https://github.com/DSpace/DSpace/tree/main/dspace/src/main/docker for usage details # -# - note: default tag for branch: dspace/dspace: dspace/dspace:latest +# - note: default tag for branch: dspace/dspace: dspace/dspace:dspace-8_x # This Dockerfile uses JDK17 by default. # To build with other versions, use "--build-arg JDK_VERSION=[value]" ARG JDK_VERSION=17 -ARG DSPACE_VERSION=latest +# The Docker version tag to build from +ARG DSPACE_VERSION=dspace-8_x +# 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 diff --git a/Dockerfile.cli b/Dockerfile.cli index 7dd35c9651d9..1f08357b3bba 100644 --- a/Dockerfile.cli +++ b/Dockerfile.cli @@ -1,15 +1,19 @@ # This image will be published as dspace/dspace-cli # See https://github.com/DSpace/DSpace/tree/main/dspace/src/main/docker for usage details # -# - note: default tag for branch: dspace/dspace-cli: dspace/dspace-cli:latest +# - note: default tag for branch: dspace/dspace-cli: dspace/dspace-cli:dspace-8_x # This Dockerfile uses JDK17 by default. # To build with other versions, use "--build-arg JDK_VERSION=[value]" ARG JDK_VERSION=17 -ARG DSPACE_VERSION=latest +# The Docker version tag to build from +ARG DSPACE_VERSION=dspace-8_x +# 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 diff --git a/Dockerfile.dependencies b/Dockerfile.dependencies index 2ca4d3040e98..04233cd415fa 100644 --- a/Dockerfile.dependencies +++ b/Dockerfile.dependencies @@ -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,60 @@ 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-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/* diff --git a/Dockerfile.test b/Dockerfile.test index cdfd5e83af5f..c9627e439fd7 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -1,17 +1,21 @@ # This image will be published as dspace/dspace # See https://github.com/DSpace/DSpace/tree/main/dspace/src/main/docker for usage details # -# - note: default tag for branch: dspace/dspace: dspace/dspace:latest-test +# - note: default tag for branch: dspace/dspace: dspace/dspace:8_x-test # # This image is meant for TESTING/DEVELOPMENT ONLY as it deploys the old v6 REST API under HTTP (not HTTPS) # This Dockerfile uses JDK17 by default. # To build with other versions, use "--build-arg JDK_VERSION=[value]" ARG JDK_VERSION=17 -ARG DSPACE_VERSION=latest +# The Docker version tag to build from +ARG DSPACE_VERSION=dspace-8_x +# 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"] diff --git a/LICENSES_THIRD_PARTY b/LICENSES_THIRD_PARTY index d7e928147c89..5d99bd7e426c 100644 --- a/LICENSES_THIRD_PARTY +++ b/LICENSES_THIRD_PARTY @@ -21,71 +21,72 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines Apache Software License, Version 2.0: * Ant-Contrib Tasks (ant-contrib:ant-contrib:1.0b3 - http://ant-contrib.sourceforge.net) - * AWS SDK for Java - Core (com.amazonaws:aws-java-sdk-core:1.12.261 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK for AWS KMS (com.amazonaws:aws-java-sdk-kms:1.12.261 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK for Amazon S3 (com.amazonaws:aws-java-sdk-s3:1.12.261 - https://aws.amazon.com/sdkforjava) - * JMES Path Query library (com.amazonaws:jmespath-java:1.12.261 - https://aws.amazon.com/sdkforjava) + * AWS SDK for Java - Core (com.amazonaws:aws-java-sdk-core:1.12.785 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK for AWS KMS (com.amazonaws:aws-java-sdk-kms:1.12.785 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK for Amazon S3 (com.amazonaws:aws-java-sdk-s3:1.12.785 - https://aws.amazon.com/sdkforjava) + * JMES Path Query library (com.amazonaws:jmespath-java:1.12.785 - https://aws.amazon.com/sdkforjava) * Titanium JSON-LD 1.1 (JRE11) (com.apicatalog:titanium-json-ld:1.3.2 - https://github.com/filip26/titanium-json-ld) * HPPC Collections (com.carrotsearch:hppc:0.8.1 - http://labs.carrotsearch.com/hppc.html/hppc) * com.drewnoakes:metadata-extractor (com.drewnoakes:metadata-extractor:2.19.0 - https://drewnoakes.com/code/exif/) * parso (com.epam:parso:2.0.14 - https://github.com/epam/parso) - * ClassMate (com.fasterxml:classmate:1.6.0 - https://github.com/FasterXML/java-classmate) - * Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.16.0 - https://github.com/FasterXML/jackson) - * Jackson-core (com.fasterxml.jackson.core:jackson-core:2.16.0 - https://github.com/FasterXML/jackson-core) - * jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.16.0 - https://github.com/FasterXML/jackson) - * Jackson dataformat: CBOR (com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.12.6 - http://github.com/FasterXML/jackson-dataformats-binary) + * Internet Time Utility (com.ethlo.time:itu:1.7.0 - https://github.com/ethlo/itu) + * ClassMate (com.fasterxml:classmate:1.7.0 - https://github.com/FasterXML/java-classmate) + * Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.19.1 - https://github.com/FasterXML/jackson) + * Jackson-core (com.fasterxml.jackson.core:jackson-core:2.19.1 - https://github.com/FasterXML/jackson-core) + * jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.19.1 - https://github.com/FasterXML/jackson) + * Jackson dataformat: CBOR (com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.17.2 - https://github.com/FasterXML/jackson-dataformats-binary) * Jackson dataformat: Smile (com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.15.2 - https://github.com/FasterXML/jackson-dataformats-binary) * Jackson-dataformat-TOML (com.fasterxml.jackson.dataformat:jackson-dataformat-toml:2.15.2 - https://github.com/FasterXML/jackson-dataformats-text) * Jackson-dataformat-YAML (com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.16.2 - https://github.com/FasterXML/jackson-dataformats-text) - * Jackson datatype: jdk8 (com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.15.4 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8) - * Jackson datatype: JSR310 (com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.0 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310) + * Jackson datatype: jdk8 (com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.19.1 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8) + * Jackson datatype: JSR310 (com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.19.1 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310) * Jackson Jakarta-RS: base (com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-base:2.16.2 - https://github.com/FasterXML/jackson-jakarta-rs-providers/jackson-jakarta-rs-base) * Jackson Jakarta-RS: JSON (com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-json-provider:2.16.2 - https://github.com/FasterXML/jackson-jakarta-rs-providers/jackson-jakarta-rs-json-provider) * Jackson module: Jakarta XML Bind Annotations (jakarta.xml.bind) (com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.16.2 - https://github.com/FasterXML/jackson-modules-base) - * Jackson-module-parameter-names (com.fasterxml.jackson.module:jackson-module-parameter-names:2.15.4 - https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names) - * Java UUID Generator (com.fasterxml.uuid:java-uuid-generator:4.0.1 - https://github.com/cowtowncoder/java-uuid-generator) + * Jackson-module-parameter-names (com.fasterxml.jackson.module:jackson-module-parameter-names:2.19.1 - https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names) + * Java UUID Generator (com.fasterxml.uuid:java-uuid-generator:4.1.0 - https://github.com/cowtowncoder/java-uuid-generator) * Woodstox (com.fasterxml.woodstox:woodstox-core:6.5.1 - https://github.com/FasterXML/woodstox) * zjsonpatch (com.flipkart.zjsonpatch:zjsonpatch:0.4.16 - https://github.com/flipkart-incubator/zjsonpatch/) * Caffeine cache (com.github.ben-manes.caffeine:caffeine:2.9.3 - https://github.com/ben-manes/caffeine) - * Caffeine cache (com.github.ben-manes.caffeine:caffeine:3.1.6 - https://github.com/ben-manes/caffeine) + * Caffeine cache (com.github.ben-manes.caffeine:caffeine:3.1.8 - https://github.com/ben-manes/caffeine) + * JSON.simple (com.github.cliftonlabs:json-simple:3.0.2 - https://cliftonlabs.github.io/json-simple/) * btf (com.github.java-json-tools:btf:1.3 - https://github.com/java-json-tools/btf) * jackson-coreutils (com.github.java-json-tools:jackson-coreutils:2.0 - https://github.com/java-json-tools/jackson-coreutils) * jackson-coreutils-equivalence (com.github.java-json-tools:jackson-coreutils-equivalence:1.0 - https://github.com/java-json-tools/jackson-coreutils) + * json-patch (com.github.java-json-tools:json-patch:1.13 - https://github.com/java-json-tools/json-patch) * json-schema-core (com.github.java-json-tools:json-schema-core:1.2.14 - https://github.com/java-json-tools/json-schema-core) * json-schema-validator (com.github.java-json-tools:json-schema-validator:2.2.14 - https://github.com/java-json-tools/json-schema-validator) * msg-simple (com.github.java-json-tools:msg-simple:1.2 - https://github.com/java-json-tools/msg-simple) * uri-template (com.github.java-json-tools:uri-template:0.10 - https://github.com/java-json-tools/uri-template) * JCIP Annotations under Apache License (com.github.stephenc.jcip:jcip-annotations:1.0-1 - http://stephenc.github.com/jcip-annotations) - * Google APIs Client Library for Java (com.google.api-client:google-api-client:1.23.0 - https://github.com/google/google-api-java-client/google-api-client) - * Google Analytics API v3-rev145-1.23.0 (com.google.apis:google-api-services-analytics:v3-rev145-1.23.0 - http://nexus.sonatype.org/oss-repository-hosting.html/google-api-services-analytics) * FindBugs-jsr305 (com.google.code.findbugs:jsr305:3.0.2 - http://findbugs.sourceforge.net/) - * Gson (com.google.code.gson:gson:2.10.1 - https://github.com/google/gson/gson) - * error-prone annotations (com.google.errorprone:error_prone_annotations:2.10.0 - https://errorprone.info/error_prone_annotations) + * Gson (com.google.code.gson:gson:2.13.1 - https://github.com/google/gson) + * error-prone annotations (com.google.errorprone:error_prone_annotations:2.38.0 - https://errorprone.info/error_prone_annotations) * Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.1 - https://github.com/google/guava/failureaccess) - * Guava: Google Core Libraries for Java (com.google.guava:guava:32.0.0-jre - https://github.com/google/guava) - * Guava: Google Core Libraries for Java (JDK5 Backport) (com.google.guava:guava-jdk5:17.0 - http://code.google.com/p/guava-libraries/guava-jdk5) + * Guava: Google Core Libraries for Java (com.google.guava:guava:32.1.3-jre - https://github.com/google/guava) * Guava ListenableFuture only (com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava - https://github.com/google/guava/listenablefuture) - * Google HTTP Client Library for Java (com.google.http-client:google-http-client:1.23.0 - https://github.com/google/google-http-java-client/google-http-client) - * GSON extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-gson:1.41.7 - https://github.com/googleapis/google-http-java-client/google-http-client-gson) - * Jackson 2 extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-jackson2:1.23.0 - https://github.com/google/google-http-java-client/google-http-client-jackson2) * J2ObjC Annotations (com.google.j2objc:j2objc-annotations:1.3 - https://github.com/google/j2objc/) * J2ObjC Annotations (com.google.j2objc:j2objc-annotations:2.8 - https://github.com/google/j2objc/) - * Google OAuth Client Library for Java (com.google.oauth-client:google-oauth-client:1.33.3 - https://github.com/googleapis/google-oauth-java-client/google-oauth-client) - * ConcurrentLinkedHashMap (com.googlecode.concurrentlinkedhashmap:concurrentlinkedhashmap-lru:1.4.2 - http://code.google.com/p/concurrentlinkedhashmap) * libphonenumber (com.googlecode.libphonenumber:libphonenumber:8.11.1 - https://github.com/google/libphonenumber/) - * Jackcess (com.healthmarketscience.jackcess:jackcess:4.0.5 - https://jackcess.sourceforge.io) - * Jackcess Encrypt (com.healthmarketscience.jackcess:jackcess-encrypt:4.0.2 - http://jackcessencrypt.sf.net) + * Jackcess (com.healthmarketscience.jackcess:jackcess:4.0.8 - https://jackcess.sourceforge.io) + * Jackcess Encrypt (com.healthmarketscience.jackcess:jackcess-encrypt:4.0.3 - http://jackcessencrypt.sf.net) * json-path (com.jayway.jsonpath:json-path:2.9.0 - https://github.com/jayway/JsonPath) * json-path-assert (com.jayway.jsonpath:json-path-assert:2.9.0 - https://github.com/jayway/JsonPath) * Disruptor Framework (com.lmax:disruptor:3.4.2 - http://lmax-exchange.github.com/disruptor) * MaxMind DB Reader (com.maxmind.db:maxmind-db:2.1.0 - http://dev.maxmind.com/) * MaxMind GeoIP2 API (com.maxmind.geoip2:geoip2:2.17.0 - https://dev.maxmind.com/geoip?lang=en) - * Nimbus JOSE+JWT (com.nimbusds:nimbus-jose-jwt:9.37.3 - https://bitbucket.org/connect2id/nimbus-jose-jwt) - * opencsv (com.opencsv:opencsv:5.9 - http://opencsv.sf.net) + * JsonSchemaValidator (com.networknt:json-schema-validator:1.0.76 - https://github.com/networknt/json-schema-validator) + * Nimbus JOSE+JWT (com.nimbusds:nimbus-jose-jwt:9.28 - https://bitbucket.org/connect2id/nimbus-jose-jwt) + * Nimbus JOSE+JWT (com.nimbusds:nimbus-jose-jwt:9.48 - https://bitbucket.org/connect2id/nimbus-jose-jwt) + * opencsv (com.opencsv:opencsv:5.11.1 - http://opencsv.sf.net) * java-libpst (com.pff:java-libpst:0.9.3 - https://github.com/rjohnsondev/java-libpst) * rome (com.rometools:rome:1.19.0 - http://rometools.com/rome) * rome-modules (com.rometools:rome-modules:1.19.0 - http://rometools.com/rome-modules) * rome-utils (com.rometools:rome-utils:1.19.0 - http://rometools.com/rome-utils) + * mockwebserver (com.squareup.okhttp3:mockwebserver:4.12.0 - https://square.github.io/okhttp/) + * okhttp (com.squareup.okhttp3:okhttp:4.12.0 - https://square.github.io/okhttp/) + * okio (com.squareup.okio:okio:3.6.0 - https://github.com/square/okio/) + * okio (com.squareup.okio:okio-jvm:3.6.0 - https://github.com/square/okio/) * T-Digest (com.tdunning:t-digest:3.1 - https://github.com/tdunning/t-digest) * config (com.typesafe:config:1.3.3 - https://github.com/lightbend/config) * ssl-config-core (com.typesafe:ssl-config-core_2.13:0.3.8 - https://github.com/lightbend/ssl-config) @@ -98,15 +99,15 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * scala-logging (com.typesafe.scala-logging:scala-logging_2.13:3.9.2 - https://github.com/lightbend/scala-logging) * JSON library from Android SDK (com.vaadin.external.google:android-json:0.0.20131108.vaadin1 - http://developer.android.com/sdk) * SparseBitSet (com.zaxxer:SparseBitSet:1.3 - https://github.com/brettwooldridge/SparseBitSet) - * Apache Commons BeanUtils (commons-beanutils:commons-beanutils:1.9.4 - https://commons.apache.org/proper/commons-beanutils/) - * Apache Commons CLI (commons-cli:commons-cli:1.6.0 - https://commons.apache.org/proper/commons-cli/) - * Apache Commons Codec (commons-codec:commons-codec:1.16.0 - https://commons.apache.org/proper/commons-codec/) + * Apache Commons BeanUtils (commons-beanutils:commons-beanutils:1.11.0 - https://commons.apache.org/proper/commons-beanutils) + * Apache Commons CLI (commons-cli:commons-cli:1.9.0 - https://commons.apache.org/proper/commons-cli/) + * Apache Commons Codec (commons-codec:commons-codec:1.18.0 - https://commons.apache.org/proper/commons-codec/) * Apache Commons Collections (commons-collections:commons-collections:3.2.2 - http://commons.apache.org/collections/) * Commons Digester (commons-digester:commons-digester:2.1 - http://commons.apache.org/digester/) - * Apache Commons IO (commons-io:commons-io:2.15.1 - https://commons.apache.org/proper/commons-io/) + * Apache Commons IO (commons-io:commons-io:2.19.0 - https://commons.apache.org/proper/commons-io/) * Commons Lang (commons-lang:commons-lang:2.6 - http://commons.apache.org/lang/) - * Apache Commons Logging (commons-logging:commons-logging:1.3.0 - https://commons.apache.org/proper/commons-logging/) - * Apache Commons Validator (commons-validator:commons-validator:1.7 - http://commons.apache.org/proper/commons-validator/) + * Apache Commons Logging (commons-logging:commons-logging:1.3.5 - https://commons.apache.org/proper/commons-logging/) + * Apache Commons Validator (commons-validator:commons-validator:1.9.0 - http://commons.apache.org/proper/commons-validator/) * GeoJson POJOs for Jackson (de.grundid.opendatalab:geojson-jackson:1.14 - https://github.com/opendatalab-de/geojson-jackson) * broker-client (eu.openaire:broker-client:1.1.2 - http://api.openaire.eu/broker/broker-client) * OpenAIRE Funders Model (eu.openaire:funders-model:2.0.0 - https://api.openaire.eu) @@ -116,84 +117,90 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Metrics Integration with JMX (io.dropwizard.metrics:metrics-jmx:4.1.5 - https://metrics.dropwizard.io/metrics-jmx) * JVM Integration for Metrics (io.dropwizard.metrics:metrics-jvm:4.1.5 - https://metrics.dropwizard.io/metrics-jvm) * SWORD v2 Common Server Library (forked) (io.gdcc:sword2-server:2.0.0 - https://github.com/gdcc/sword2-server) - * micrometer-commons (io.micrometer:micrometer-commons:1.12.6 - https://github.com/micrometer-metrics/micrometer) - * micrometer-core (io.micrometer:micrometer-core:1.12.6 - https://github.com/micrometer-metrics/micrometer) - * micrometer-jakarta9 (io.micrometer:micrometer-jakarta9:1.12.6 - https://github.com/micrometer-metrics/micrometer) - * micrometer-observation (io.micrometer:micrometer-observation:1.12.6 - https://github.com/micrometer-metrics/micrometer) - * Netty/Buffer (io.netty:netty-buffer:4.1.106.Final - https://netty.io/netty-buffer/) + * micrometer-commons (io.micrometer:micrometer-commons:1.14.8 - https://github.com/micrometer-metrics/micrometer) + * micrometer-core (io.micrometer:micrometer-core:1.15.1 - https://github.com/micrometer-metrics/micrometer) + * micrometer-jakarta9 (io.micrometer:micrometer-jakarta9:1.15.1 - https://github.com/micrometer-metrics/micrometer) + * micrometer-observation (io.micrometer:micrometer-observation:1.14.8 - https://github.com/micrometer-metrics/micrometer) * Netty/Buffer (io.netty:netty-buffer:4.1.99.Final - https://netty.io/netty-buffer/) - * Netty/Codec (io.netty:netty-codec:4.1.106.Final - https://netty.io/netty-codec/) * Netty/Codec (io.netty:netty-codec:4.1.99.Final - https://netty.io/netty-codec/) - * Netty/Codec/HTTP (io.netty:netty-codec-http:4.1.53.Final - https://netty.io/netty-codec-http/) - * Netty/Codec/Socks (io.netty:netty-codec-socks:4.1.53.Final - https://netty.io/netty-codec-socks/) - * Netty/Common (io.netty:netty-common:4.1.106.Final - https://netty.io/netty-common/) + * Netty/Codec/HTTP (io.netty:netty-codec-http:4.1.86.Final - https://netty.io/netty-codec-http/) + * Netty/Codec/HTTP2 (io.netty:netty-codec-http2:4.1.86.Final - https://netty.io/netty-codec-http2/) + * Netty/Codec/Socks (io.netty:netty-codec-socks:4.1.86.Final - https://netty.io/netty-codec-socks/) * Netty/Common (io.netty:netty-common:4.1.99.Final - https://netty.io/netty-common/) - * Netty/Handler (io.netty:netty-handler:4.1.106.Final - https://netty.io/netty-handler/) * Netty/Handler (io.netty:netty-handler:4.1.99.Final - https://netty.io/netty-handler/) - * Netty/Handler/Proxy (io.netty:netty-handler-proxy:4.1.53.Final - https://netty.io/netty-handler-proxy/) + * Netty/Handler/Proxy (io.netty:netty-handler-proxy:4.1.86.Final - https://netty.io/netty-handler-proxy/) * Netty/Resolver (io.netty:netty-resolver:4.1.99.Final - https://netty.io/netty-resolver/) - * Netty/Transport (io.netty:netty-transport:4.1.106.Final - https://netty.io/netty-transport/) + * Netty/TomcatNative [BoringSSL - Static] (io.netty:netty-tcnative-boringssl-static:2.0.56.Final - https://github.com/netty/netty-tcnative/netty-tcnative-boringssl-static/) + * Netty/TomcatNative [OpenSSL - Classes] (io.netty:netty-tcnative-classes:2.0.56.Final - https://github.com/netty/netty-tcnative/netty-tcnative-classes/) * Netty/Transport (io.netty:netty-transport:4.1.99.Final - https://netty.io/netty-transport/) + * Netty/Transport/Classes/Epoll (io.netty:netty-transport-classes-epoll:4.1.99.Final - https://netty.io/netty-transport-classes-epoll/) * Netty/Transport/Native/Epoll (io.netty:netty-transport-native-epoll:4.1.99.Final - https://netty.io/netty-transport-native-epoll/) - * Netty/Transport/Native/Unix/Common (io.netty:netty-transport-native-unix-common:4.1.106.Final - https://netty.io/netty-transport-native-unix-common/) * Netty/Transport/Native/Unix/Common (io.netty:netty-transport-native-unix-common:4.1.99.Final - https://netty.io/netty-transport-native-unix-common/) * OpenTracing API (io.opentracing:opentracing-api:0.33.0 - https://github.com/opentracing/opentracing-java/opentracing-api) * OpenTracing-noop (io.opentracing:opentracing-noop:0.33.0 - https://github.com/opentracing/opentracing-java/opentracing-noop) * OpenTracing-util (io.opentracing:opentracing-util:0.33.0 - https://github.com/opentracing/opentracing-java/opentracing-util) + * Prometheus Java Simpleclient (io.prometheus:simpleclient:0.16.0 - http://github.com/prometheus/client_java/simpleclient) + * Prometheus Java Simpleclient Common (io.prometheus:simpleclient_common:0.16.0 - http://github.com/prometheus/client_java/simpleclient_common) + * Prometheus Java Simpleclient Httpserver (io.prometheus:simpleclient_httpserver:0.16.0 - http://github.com/prometheus/client_java/simpleclient_httpserver) + * Prometheus Java Span Context Supplier - Common (io.prometheus:simpleclient_tracer_common:0.16.0 - http://github.com/prometheus/client_java/simpleclient_tracer/simpleclient_tracer_common) + * Prometheus Java Span Context Supplier - OpenTelemetry (io.prometheus:simpleclient_tracer_otel:0.16.0 - http://github.com/prometheus/client_java/simpleclient_tracer/simpleclient_tracer_otel) + * Prometheus Java Span Context Supplier - OpenTelemetry Agent (io.prometheus:simpleclient_tracer_otel_agent:0.16.0 - http://github.com/prometheus/client_java/simpleclient_tracer/simpleclient_tracer_otel_agent) * Google S2 geometry library (io.sgr:s2-geometry-library-java:1.0.0 - https://github.com/sgr-io/s2-geometry-library-java) * Jandex: Core (io.smallrye:jandex:3.1.2 - https://smallrye.io) - * swagger-annotations (io.swagger:swagger-annotations:1.6.2 - https://github.com/swagger-api/swagger-core/modules/swagger-annotations) - * swagger-compat-spec-parser (io.swagger:swagger-compat-spec-parser:1.0.52 - http://nexus.sonatype.org/oss-repository-hosting.html/swagger-parser-project/modules/swagger-compat-spec-parser) - * swagger-core (io.swagger:swagger-core:1.6.2 - https://github.com/swagger-api/swagger-core/modules/swagger-core) - * swagger-models (io.swagger:swagger-models:1.6.2 - https://github.com/swagger-api/swagger-core/modules/swagger-models) - * swagger-parser (io.swagger:swagger-parser:1.0.52 - http://nexus.sonatype.org/oss-repository-hosting.html/swagger-parser-project/modules/swagger-parser) - * swagger-annotations (io.swagger.core.v3:swagger-annotations:2.1.5 - https://github.com/swagger-api/swagger-core/modules/swagger-annotations) + * swagger-annotations (io.swagger:swagger-annotations:1.6.9 - https://github.com/swagger-api/swagger-core/modules/swagger-annotations) + * swagger-compat-spec-parser (io.swagger:swagger-compat-spec-parser:1.0.64 - http://nexus.sonatype.org/oss-repository-hosting.html/swagger-parser-project/modules/swagger-compat-spec-parser) + * swagger-core (io.swagger:swagger-core:1.6.9 - https://github.com/swagger-api/swagger-core/modules/swagger-core) + * swagger-models (io.swagger:swagger-models:1.6.9 - https://github.com/swagger-api/swagger-core/modules/swagger-models) + * swagger-parser (io.swagger:swagger-parser:1.0.64 - http://nexus.sonatype.org/oss-repository-hosting.html/swagger-parser-project/modules/swagger-parser) + * swagger-annotations (io.swagger.core.v3:swagger-annotations:2.2.8 - https://github.com/swagger-api/swagger-core/modules/swagger-annotations) * swagger-annotations-jakarta (io.swagger.core.v3:swagger-annotations-jakarta:2.2.21 - https://github.com/swagger-api/swagger-core/modules/swagger-annotations-jakarta) - * swagger-core (io.swagger.core.v3:swagger-core:2.1.5 - https://github.com/swagger-api/swagger-core/modules/swagger-core) + * swagger-core (io.swagger.core.v3:swagger-core:2.2.8 - https://github.com/swagger-api/swagger-core/modules/swagger-core) * swagger-core-jakarta (io.swagger.core.v3:swagger-core-jakarta:2.2.21 - https://github.com/swagger-api/swagger-core/modules/swagger-core-jakarta) * swagger-integration-jakarta (io.swagger.core.v3:swagger-integration-jakarta:2.2.21 - https://github.com/swagger-api/swagger-core/modules/swagger-integration-jakarta) * swagger-jaxrs2-jakarta (io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.21 - https://github.com/swagger-api/swagger-core/modules/swagger-jaxrs2-jakarta) - * swagger-models (io.swagger.core.v3:swagger-models:2.1.5 - https://github.com/swagger-api/swagger-core/modules/swagger-models) + * swagger-models (io.swagger.core.v3:swagger-models:2.2.8 - https://github.com/swagger-api/swagger-core/modules/swagger-models) * swagger-models-jakarta (io.swagger.core.v3:swagger-models-jakarta:2.2.21 - https://github.com/swagger-api/swagger-core/modules/swagger-models-jakarta) - * swagger-parser (io.swagger.parser.v3:swagger-parser:2.0.23 - http://nexus.sonatype.org/oss-repository-hosting.html/swagger-parser-project/modules/swagger-parser) - * swagger-parser (io.swagger.parser.v3:swagger-parser-core:2.0.23 - http://nexus.sonatype.org/oss-repository-hosting.html/swagger-parser-project/modules/swagger-parser-core) - * swagger-parser-v2-converter (io.swagger.parser.v3:swagger-parser-v2-converter:2.0.23 - http://nexus.sonatype.org/oss-repository-hosting.html/swagger-parser-project/modules/swagger-parser-v2-converter) - * swagger-parser-v3 (io.swagger.parser.v3:swagger-parser-v3:2.0.23 - http://nexus.sonatype.org/oss-repository-hosting.html/swagger-parser-project/modules/swagger-parser-v3) + * swagger-parser (io.swagger.parser.v3:swagger-parser:2.1.10 - http://nexus.sonatype.org/oss-repository-hosting.html/swagger-parser-project/modules/swagger-parser) + * swagger-parser (io.swagger.parser.v3:swagger-parser-core:2.1.10 - http://nexus.sonatype.org/oss-repository-hosting.html/swagger-parser-project/modules/swagger-parser-core) + * swagger-parser-v2-converter (io.swagger.parser.v3:swagger-parser-v2-converter:2.1.10 - http://nexus.sonatype.org/oss-repository-hosting.html/swagger-parser-project/modules/swagger-parser-v2-converter) + * swagger-parser-v3 (io.swagger.parser.v3:swagger-parser-v3:2.1.10 - http://nexus.sonatype.org/oss-repository-hosting.html/swagger-parser-project/modules/swagger-parser-v3) * Jakarta Dependency Injection (jakarta.inject:jakarta.inject-api:2.0.1 - https://github.com/eclipse-ee4j/injection-api) * Jakarta Bean Validation API (jakarta.validation:jakarta.validation-api:3.0.2 - https://beanvalidation.org) * JSR107 API and SPI (javax.cache:cache-api:1.1.1 - https://github.com/jsr107/jsr107spec) - * javax.inject (javax.inject:javax.inject:1 - http://code.google.com/p/atinject/) - * Bean Validation API (javax.validation:validation-api:1.1.0.Final - http://beanvalidation.org) * jdbm (jdbm:jdbm:1.0 - no url defined) - * Joda-Time (joda-time:joda-time:2.12.5 - https://www.joda.org/joda-time/) + * Joda-Time (joda-time:joda-time:2.12.7 - https://www.joda.org/joda-time/) * Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.11.13 - https://bytebuddy.net/byte-buddy) + * Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.14.11 - https://bytebuddy.net/byte-buddy) * Byte Buddy agent (net.bytebuddy:byte-buddy-agent:1.11.13 - https://bytebuddy.net/byte-buddy-agent) * eigenbase-properties (net.hydromatic:eigenbase-properties:1.1.5 - http://github.com/julianhyde/eigenbase-properties) - * json-unit-core (net.javacrumbs.json-unit:json-unit-core:2.19.0 - https://github.com/lukas-krecan/JsonUnit/json-unit-core) + * json-unit-core (net.javacrumbs.json-unit:json-unit-core:2.36.0 - https://github.com/lukas-krecan/JsonUnit/json-unit-core) * "Java Concurrency in Practice" book annotations (net.jcip:jcip-annotations:1.0 - http://jcip.net/) * ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.5.0 - https://urielch.github.io/) + * ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.5.2 - https://urielch.github.io/) * JSON Small and Fast Parser (net.minidev:json-smart:2.5.0 - https://urielch.github.io/) + * JSON Small and Fast Parser (net.minidev:json-smart:2.5.2 - https://urielch.github.io/) * Abdera Core (org.apache.abdera:abdera-core:1.1.3 - http://abdera.apache.org/abdera-core) * I18N Libraries (org.apache.abdera:abdera-i18n:1.1.3 - http://abdera.apache.org) * Abdera Parser (org.apache.abdera:abdera-parser:1.1.3 - http://abdera.apache.org/abdera-parser) - * Apache Ant Core (org.apache.ant:ant:1.10.14 - https://ant.apache.org/) - * Apache Ant Launcher (org.apache.ant:ant-launcher:1.10.14 - https://ant.apache.org/) - * Apache Commons BCEL (org.apache.bcel:bcel:6.7.0 - https://commons.apache.org/proper/commons-bcel) + * Apache Ant Core (org.apache.ant:ant:1.10.15 - https://ant.apache.org/) + * Apache Ant Launcher (org.apache.ant:ant-launcher:1.10.15 - https://ant.apache.org/) + * Apache Commons BCEL (org.apache.bcel:bcel:6.10.0 - https://commons.apache.org/proper/commons-bcel) * Calcite Core (org.apache.calcite:calcite-core:1.35.0 - https://calcite.apache.org) * Calcite Linq4j (org.apache.calcite:calcite-linq4j:1.35.0 - https://calcite.apache.org) * Apache Calcite Avatica (org.apache.calcite.avatica:avatica-core:1.23.0 - https://calcite.apache.org/avatica) * Apache Calcite Avatica Metrics (org.apache.calcite.avatica:avatica-metrics:1.23.0 - https://calcite.apache.org/avatica) - * Apache Commons Collections (org.apache.commons:commons-collections4:4.4 - https://commons.apache.org/proper/commons-collections/) - * Apache Commons Compress (org.apache.commons:commons-compress:1.26.0 - https://commons.apache.org/proper/commons-compress/) - * Apache Commons Configuration (org.apache.commons:commons-configuration2:2.10.1 - https://commons.apache.org/proper/commons-configuration/) - * Apache Commons CSV (org.apache.commons:commons-csv:1.10.0 - https://commons.apache.org/proper/commons-csv/) - * Apache Commons DBCP (org.apache.commons:commons-dbcp2:2.11.0 - https://commons.apache.org/dbcp/) + * Apache Commons Collections (org.apache.commons:commons-collections4:4.5.0 - https://commons.apache.org/proper/commons-collections/) + * Apache Commons Compress (org.apache.commons:commons-compress:1.27.1 - https://commons.apache.org/proper/commons-compress/) + * Apache Commons Configuration (org.apache.commons:commons-configuration2:2.12.0 - https://commons.apache.org/proper/commons-configuration/) + * Apache Commons CSV (org.apache.commons:commons-csv:1.14.0 - https://commons.apache.org/proper/commons-csv/) + * Apache Commons DBCP (org.apache.commons:commons-dbcp2:2.13.0 - https://commons.apache.org/proper/commons-dbcp/) + * Apache Commons Digester (org.apache.commons:commons-digester3:3.2 - http://commons.apache.org/digester/) * Apache Commons Exec (org.apache.commons:commons-exec:1.3 - http://commons.apache.org/proper/commons-exec/) * Apache Commons Exec (org.apache.commons:commons-exec:1.4.0 - https://commons.apache.org/proper/commons-exec/) - * Apache Commons Lang (org.apache.commons:commons-lang3:3.14.0 - https://commons.apache.org/proper/commons-lang/) + * Apache Commons Lang (org.apache.commons:commons-lang3:3.17.0 - https://commons.apache.org/proper/commons-lang/) * Apache Commons Math (org.apache.commons:commons-math3:3.6.1 - http://commons.apache.org/proper/commons-math/) - * Apache Commons Pool (org.apache.commons:commons-pool2:2.12.0 - https://commons.apache.org/proper/commons-pool/) - * Apache Commons Text (org.apache.commons:commons-text:1.10.0 - https://commons.apache.org/proper/commons-text) + * Apache Commons Pool (org.apache.commons:commons-pool2:2.12.1 - https://commons.apache.org/proper/commons-pool/) + * Apache Commons Text (org.apache.commons:commons-text:1.13.1 - https://commons.apache.org/proper/commons-text) * Curator Client (org.apache.curator:curator-client:2.13.0 - http://curator.apache.org/curator-client) * Curator Framework (org.apache.curator:curator-framework:2.13.0 - http://curator.apache.org/curator-framework) * Curator Recipes (org.apache.curator:curator-recipes:2.13.0 - http://curator.apache.org/curator-recipes) @@ -207,117 +214,119 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Apache HttpCore (org.apache.httpcomponents:httpcore:4.4.16 - http://hc.apache.org/httpcomponents-core-ga) * Apache HttpClient Mime (org.apache.httpcomponents:httpmime:4.5.14 - http://hc.apache.org/httpcomponents-client-ga) * Apache HttpClient (org.apache.httpcomponents.client5:httpclient5:5.1.3 - https://hc.apache.org/httpcomponents-client-5.0.x/5.1.3/httpclient5/) - * Apache HttpClient (org.apache.httpcomponents.client5:httpclient5:5.3.1 - https://hc.apache.org/httpcomponents-client-5.0.x/5.3.1/httpclient5/) + * Apache HttpClient (org.apache.httpcomponents.client5:httpclient5:5.5 - https://hc.apache.org/httpcomponents-client-5.5.x/5.5/httpclient5/) * Apache HttpComponents Core HTTP/1.1 (org.apache.httpcomponents.core5:httpcore5:5.1.3 - https://hc.apache.org/httpcomponents-core-5.1.x/5.1.3/httpcore5/) - * Apache HttpComponents Core HTTP/1.1 (org.apache.httpcomponents.core5:httpcore5:5.2.4 - https://hc.apache.org/httpcomponents-core-5.2.x/5.2.4/httpcore5/) + * Apache HttpComponents Core HTTP/1.1 (org.apache.httpcomponents.core5:httpcore5:5.3.4 - https://hc.apache.org/httpcomponents-core-5.3.x/5.3.4/httpcore5/) * Apache HttpComponents Core HTTP/2 (org.apache.httpcomponents.core5:httpcore5-h2:5.1.3 - https://hc.apache.org/httpcomponents-core-5.1.x/5.1.3/httpcore5-h2/) - * Apache HttpComponents Core HTTP/2 (org.apache.httpcomponents.core5:httpcore5-h2:5.2.4 - https://hc.apache.org/httpcomponents-core-5.2.x/5.2.4/httpcore5-h2/) - * Apache James :: Mime4j :: Core (org.apache.james:apache-mime4j-core:0.8.10 - http://james.apache.org/mime4j/apache-mime4j-core) - * Apache James :: Mime4j :: DOM (org.apache.james:apache-mime4j-dom:0.8.11 - http://james.apache.org/mime4j/apache-mime4j-dom) - * Apache Jena - Libraries POM (org.apache.jena:apache-jena-libs:4.9.0 - https://jena.apache.org/apache-jena-libs/) - * Apache Jena - ARQ (org.apache.jena:jena-arq:4.9.0 - https://jena.apache.org/jena-arq/) - * Apache Jena - Base (org.apache.jena:jena-base:4.9.0 - https://jena.apache.org/jena-base/) - * Apache Jena - Core (org.apache.jena:jena-core:4.9.0 - https://jena.apache.org/jena-core/) - * Apache Jena - DBOE Base (org.apache.jena:jena-dboe-base:4.9.0 - https://jena.apache.org/jena-dboe-base/) - * Apache Jena - DBOE Indexes (org.apache.jena:jena-dboe-index:4.9.0 - https://jena.apache.org/jena-dboe-index/) - * Apache Jena - DBOE Storage (org.apache.jena:jena-dboe-storage:4.9.0 - https://jena.apache.org/jena-dboe-storage/) - * Apache Jena - DBOE Transactional Datastructures (org.apache.jena:jena-dboe-trans-data:4.9.0 - https://jena.apache.org/jena-dboe-trans-data/) - * Apache Jena - DBOE Transactions (org.apache.jena:jena-dboe-transaction:4.9.0 - https://jena.apache.org/jena-dboe-transaction/) - * Apache Jena - IRI (org.apache.jena:jena-iri:4.9.0 - https://jena.apache.org/jena-iri/) - * Apache Jena - RDF Connection (org.apache.jena:jena-rdfconnection:4.9.0 - https://jena.apache.org/jena-rdfconnection/) - * Apache Jena - RDF Patch (org.apache.jena:jena-rdfpatch:4.9.0 - https://jena.apache.org/jena-rdfpatch/) - * Apache Jena - SHACL (org.apache.jena:jena-shacl:4.9.0 - https://jena.apache.org/jena-shacl/) - * Apache Jena - ShEx (org.apache.jena:jena-shex:4.9.0 - https://jena.apache.org/jena-shex/) - * Apache Jena - TDB1 (Native Triple Store) (org.apache.jena:jena-tdb:4.9.0 - https://jena.apache.org/jena-tdb/) - * Apache Jena - TDB2 (Native Triple Store) (org.apache.jena:jena-tdb2:4.9.0 - https://jena.apache.org/jena-tdb2/) + * Apache HttpComponents Core HTTP/2 (org.apache.httpcomponents.core5:httpcore5-h2:5.3.4 - https://hc.apache.org/httpcomponents-core-5.3.x/5.3.4/httpcore5-h2/) + * Apache James :: Mime4j :: Core (org.apache.james:apache-mime4j-core:0.8.12 - http://james.apache.org/mime4j/apache-mime4j-core) + * Apache James :: Mime4j :: DOM (org.apache.james:apache-mime4j-dom:0.8.12 - http://james.apache.org/mime4j/apache-mime4j-dom) + * Apache Jena - Libraries POM (org.apache.jena:apache-jena-libs:4.10.0 - https://jena.apache.org/apache-jena-libs/) + * Apache Jena - ARQ (org.apache.jena:jena-arq:4.10.0 - https://jena.apache.org/jena-arq/) + * Apache Jena - Base (org.apache.jena:jena-base:4.10.0 - https://jena.apache.org/jena-base/) + * Apache Jena - Core (org.apache.jena:jena-core:4.10.0 - https://jena.apache.org/jena-core/) + * Apache Jena - DBOE Base (org.apache.jena:jena-dboe-base:4.10.0 - https://jena.apache.org/jena-dboe-base/) + * Apache Jena - DBOE Indexes (org.apache.jena:jena-dboe-index:4.10.0 - https://jena.apache.org/jena-dboe-index/) + * Apache Jena - DBOE Storage (org.apache.jena:jena-dboe-storage:4.10.0 - https://jena.apache.org/jena-dboe-storage/) + * Apache Jena - DBOE Transactional Datastructures (org.apache.jena:jena-dboe-trans-data:4.10.0 - https://jena.apache.org/jena-dboe-trans-data/) + * Apache Jena - DBOE Transactions (org.apache.jena:jena-dboe-transaction:4.10.0 - https://jena.apache.org/jena-dboe-transaction/) + * Apache Jena - IRI (org.apache.jena:jena-iri:4.10.0 - https://jena.apache.org/jena-iri/) + * Apache Jena - RDF Connection (org.apache.jena:jena-rdfconnection:4.10.0 - https://jena.apache.org/jena-rdfconnection/) + * Apache Jena - RDF Patch (org.apache.jena:jena-rdfpatch:4.10.0 - https://jena.apache.org/jena-rdfpatch/) + * Apache Jena - SHACL (org.apache.jena:jena-shacl:4.10.0 - https://jena.apache.org/jena-shacl/) + * Apache Jena - ShEx (org.apache.jena:jena-shex:4.10.0 - https://jena.apache.org/jena-shex/) + * Apache Jena - TDB1 (Native Triple Store) (org.apache.jena:jena-tdb:4.10.0 - https://jena.apache.org/jena-tdb/) + * Apache Jena - TDB2 (Native Triple Store) (org.apache.jena:jena-tdb2:4.10.0 - https://jena.apache.org/jena-tdb2/) * Kerby-kerb core (org.apache.kerby:kerb-core:1.0.1 - http://directory.apache.org/kerby/kerby-kerb/kerb-core) * Kerby-kerb Util (org.apache.kerby:kerb-util:1.0.1 - http://directory.apache.org/kerby/kerby-kerb/kerb-util) * Kerby ASN1 Project (org.apache.kerby:kerby-asn1:1.0.1 - http://directory.apache.org/kerby/kerby-common/kerby-asn1) * Kerby PKIX Project (org.apache.kerby:kerby-pkix:1.0.1 - http://directory.apache.org/kerby/kerby-pkix) - * Apache Log4j 1.x Compatibility API (org.apache.logging.log4j:log4j-1.2-api:2.23.1 - https://logging.apache.org/log4j/2.x/log4j/log4j-1.2-api/) - * Apache Log4j API (org.apache.logging.log4j:log4j-api:2.23.1 - https://logging.apache.org/log4j/2.x/log4j/log4j-api/) - * Apache Log4j Core (org.apache.logging.log4j:log4j-core:2.23.1 - https://logging.apache.org/log4j/2.x/log4j/log4j-core/) - * Apache Log4j JUL Adapter (org.apache.logging.log4j:log4j-jul:2.23.1 - https://logging.apache.org/log4j/2.x/log4j/log4j-jul/) + * Apache Log4j 1.x Compatibility API (org.apache.logging.log4j:log4j-1.2-api:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-1.2-api/) + * Apache Log4j API (org.apache.logging.log4j:log4j-api:2.24.3 - https://logging.apache.org/log4j/2.x/log4j/log4j-api/) + * Apache Log4j Core (org.apache.logging.log4j:log4j-core:2.24.3 - https://logging.apache.org/log4j/2.x/log4j/log4j-core/) + * Apache Log4j JUL Adapter (org.apache.logging.log4j:log4j-jul:2.24.3 - https://logging.apache.org/log4j/2.x/log4j/log4j-jul/) * Apache Log4j Layout for JSON template (org.apache.logging.log4j:log4j-layout-template-json:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-layout-template-json/) - * Apache Log4j SLF4J Binding (org.apache.logging.log4j:log4j-slf4j-impl:2.23.1 - https://logging.apache.org/log4j/2.x/log4j/log4j-slf4j-impl/) - * Apache Log4j SLF4J 2.0 Binding (org.apache.logging.log4j:log4j-slf4j2-impl:2.21.1 - https://logging.apache.org/log4j/2.x/log4j/log4j-slf4j2-impl/) - * Apache Log4j Web (org.apache.logging.log4j:log4j-web:2.23.1 - https://logging.apache.org/log4j/2.x/log4j/log4j-web/) - * Lucene Common Analyzers (org.apache.lucene:lucene-analyzers-common:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-analyzers-common) - * Lucene ICU Analysis Components (org.apache.lucene:lucene-analyzers-icu:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-analyzers-icu) - * Lucene Kuromoji Japanese Morphological Analyzer (org.apache.lucene:lucene-analyzers-kuromoji:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-analyzers-kuromoji) - * Lucene Nori Korean Morphological Analyzer (org.apache.lucene:lucene-analyzers-nori:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-analyzers-nori) - * Lucene Phonetic Filters (org.apache.lucene:lucene-analyzers-phonetic:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-analyzers-phonetic) - * Lucene Smart Chinese Analyzer (org.apache.lucene:lucene-analyzers-smartcn:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-analyzers-smartcn) - * Lucene Stempel Analyzer (org.apache.lucene:lucene-analyzers-stempel:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-analyzers-stempel) - * Lucene Memory (org.apache.lucene:lucene-backward-codecs:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-backward-codecs) - * Lucene Classification (org.apache.lucene:lucene-classification:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-classification) - * Lucene codecs (org.apache.lucene:lucene-codecs:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-codecs) - * Lucene Core (org.apache.lucene:lucene-core:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-core) - * Lucene Expressions (org.apache.lucene:lucene-expressions:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-expressions) - * Lucene Grouping (org.apache.lucene:lucene-grouping:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-grouping) - * Lucene Highlighter (org.apache.lucene:lucene-highlighter:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-highlighter) - * Lucene Join (org.apache.lucene:lucene-join:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-join) - * Lucene Memory (org.apache.lucene:lucene-memory:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-memory) - * Lucene Miscellaneous (org.apache.lucene:lucene-misc:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-misc) - * Lucene Queries (org.apache.lucene:lucene-queries:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-queries) - * Lucene QueryParsers (org.apache.lucene:lucene-queryparser:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-queryparser) - * Lucene Sandbox (org.apache.lucene:lucene-sandbox:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-sandbox) - * Lucene Spatial Extras (org.apache.lucene:lucene-spatial-extras:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-spatial-extras) - * Lucene Spatial 3D (org.apache.lucene:lucene-spatial3d:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-spatial3d) - * Lucene Suggest (org.apache.lucene:lucene-suggest:8.11.3 - https://lucene.apache.org/lucene-parent/lucene-suggest) - * Apache FontBox (org.apache.pdfbox:fontbox:2.0.31 - http://pdfbox.apache.org/) + * Apache Log4j SLF4J Binding (org.apache.logging.log4j:log4j-slf4j-impl:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-slf4j-impl/) + * SLF4J 2 Provider for Log4j API (org.apache.logging.log4j:log4j-slf4j2-impl:2.24.3 - https://logging.apache.org/log4j/2.x/log4j/log4j-slf4j2-impl/) + * Apache Log4j Web (org.apache.logging.log4j:log4j-web:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-web/) + * Lucene Common Analyzers (org.apache.lucene:lucene-analyzers-common:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-analyzers-common) + * Lucene ICU Analysis Components (org.apache.lucene:lucene-analyzers-icu:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-analyzers-icu) + * Lucene Kuromoji Japanese Morphological Analyzer (org.apache.lucene:lucene-analyzers-kuromoji:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-analyzers-kuromoji) + * Lucene Nori Korean Morphological Analyzer (org.apache.lucene:lucene-analyzers-nori:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-analyzers-nori) + * Lucene Phonetic Filters (org.apache.lucene:lucene-analyzers-phonetic:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-analyzers-phonetic) + * Lucene Smart Chinese Analyzer (org.apache.lucene:lucene-analyzers-smartcn:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-analyzers-smartcn) + * Lucene Stempel Analyzer (org.apache.lucene:lucene-analyzers-stempel:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-analyzers-stempel) + * Lucene Memory (org.apache.lucene:lucene-backward-codecs:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-backward-codecs) + * Lucene Classification (org.apache.lucene:lucene-classification:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-classification) + * Lucene codecs (org.apache.lucene:lucene-codecs:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-codecs) + * Lucene Core (org.apache.lucene:lucene-core:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-core) + * Lucene Expressions (org.apache.lucene:lucene-expressions:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-expressions) + * Lucene Grouping (org.apache.lucene:lucene-grouping:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-grouping) + * Lucene Highlighter (org.apache.lucene:lucene-highlighter:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-highlighter) + * Lucene Join (org.apache.lucene:lucene-join:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-join) + * Lucene Memory (org.apache.lucene:lucene-memory:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-memory) + * Lucene Miscellaneous (org.apache.lucene:lucene-misc:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-misc) + * Lucene Queries (org.apache.lucene:lucene-queries:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-queries) + * Lucene QueryParsers (org.apache.lucene:lucene-queryparser:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-queryparser) + * Lucene Sandbox (org.apache.lucene:lucene-sandbox:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-sandbox) + * Lucene Spatial Extras (org.apache.lucene:lucene-spatial-extras:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-spatial-extras) + * Lucene Spatial 3D (org.apache.lucene:lucene-spatial3d:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-spatial3d) + * Lucene Suggest (org.apache.lucene:lucene-suggest:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-suggest) + * Apache FontBox (org.apache.pdfbox:fontbox:2.0.34 - http://pdfbox.apache.org/) * PDFBox JBIG2 ImageIO plugin (org.apache.pdfbox:jbig2-imageio:3.0.4 - https://www.apache.org/jbig2-imageio/) * Apache JempBox (org.apache.pdfbox:jempbox:1.8.17 - http://www.apache.org/pdfbox-parent/jempbox/) - * Apache PDFBox (org.apache.pdfbox:pdfbox:2.0.31 - https://www.apache.org/pdfbox-parent/pdfbox/) - * Apache PDFBox tools (org.apache.pdfbox:pdfbox-tools:2.0.31 - https://www.apache.org/pdfbox-parent/pdfbox-tools/) - * Apache XmpBox (org.apache.pdfbox:xmpbox:2.0.31 - https://www.apache.org/pdfbox-parent/xmpbox/) - * Apache POI - Common (org.apache.poi:poi:5.2.5 - https://poi.apache.org/) - * Apache POI - API based on OPC and OOXML schemas (org.apache.poi:poi-ooxml:5.2.5 - https://poi.apache.org/) - * Apache POI (org.apache.poi:poi-ooxml-lite:5.2.5 - https://poi.apache.org/) - * Apache POI (org.apache.poi:poi-scratchpad:5.2.5 - https://poi.apache.org/) - * Apache Solr Core (org.apache.solr:solr-core:8.11.3 - https://lucene.apache.org/solr-parent/solr-core) - * Apache Solr Solrj (org.apache.solr:solr-solrj:8.11.3 - https://lucene.apache.org/solr-parent/solr-solrj) + * Apache PDFBox (org.apache.pdfbox:pdfbox:2.0.34 - https://www.apache.org/pdfbox-parent/pdfbox/) + * Apache PDFBox tools (org.apache.pdfbox:pdfbox-tools:2.0.34 - https://www.apache.org/pdfbox-parent/pdfbox-tools/) + * Apache XmpBox (org.apache.pdfbox:xmpbox:2.0.34 - https://www.apache.org/pdfbox-parent/xmpbox/) + * Apache POI - Common (org.apache.poi:poi:5.4.1 - https://poi.apache.org/) + * Apache POI - API based on OPC and OOXML schemas (org.apache.poi:poi-ooxml:5.4.1 - https://poi.apache.org/) + * Apache POI (org.apache.poi:poi-ooxml-lite:5.4.1 - https://poi.apache.org/) + * Apache POI (org.apache.poi:poi-scratchpad:5.4.1 - https://poi.apache.org/) + * Apache Solr Core (org.apache.solr:solr-core:8.11.4 - https://lucene.apache.org/solr-parent/solr-core) + * Apache Solr Solrj (org.apache.solr:solr-solrj:8.11.4 - https://lucene.apache.org/solr-parent/solr-solrj) * Apache Standard Taglib Implementation (org.apache.taglibs:taglibs-standard-impl:1.2.5 - http://tomcat.apache.org/taglibs/standard-1.2.5/taglibs-standard-impl) * Apache Standard Taglib Specification API (org.apache.taglibs:taglibs-standard-spec:1.2.5 - http://tomcat.apache.org/taglibs/standard-1.2.5/taglibs-standard-spec) - * Apache Thrift (org.apache.thrift:libthrift:0.18.1 - http://thrift.apache.org) - * Apache Tika core (org.apache.tika:tika-core:2.9.2 - https://tika.apache.org/) - * Apache Tika Apple parser module (org.apache.tika:tika-parser-apple-module:2.9.2 - https://tika.apache.org/tika-parser-apple-module/) - * Apache Tika audiovideo parser module (org.apache.tika:tika-parser-audiovideo-module:2.9.2 - https://tika.apache.org/tika-parser-audiovideo-module/) - * Apache Tika cad parser module (org.apache.tika:tika-parser-cad-module:2.9.2 - https://tika.apache.org/tika-parser-cad-module/) - * Apache Tika code parser module (org.apache.tika:tika-parser-code-module:2.9.2 - https://tika.apache.org/tika-parser-code-module/) - * Apache Tika crypto parser module (org.apache.tika:tika-parser-crypto-module:2.9.2 - https://tika.apache.org/tika-parser-crypto-module/) - * Apache Tika digest commons (org.apache.tika:tika-parser-digest-commons:2.9.2 - https://tika.apache.org/tika-parser-digest-commons/) - * Apache Tika font parser module (org.apache.tika:tika-parser-font-module:2.9.2 - https://tika.apache.org/tika-parser-font-module/) - * Apache Tika html parser module (org.apache.tika:tika-parser-html-module:2.9.2 - https://tika.apache.org/tika-parser-html-module/) - * Apache Tika image parser module (org.apache.tika:tika-parser-image-module:2.9.2 - https://tika.apache.org/tika-parser-image-module/) - * Apache Tika mail commons (org.apache.tika:tika-parser-mail-commons:2.9.2 - https://tika.apache.org/tika-parser-mail-commons/) - * Apache Tika mail parser module (org.apache.tika:tika-parser-mail-module:2.9.2 - https://tika.apache.org/tika-parser-mail-module/) - * Apache Tika Microsoft parser module (org.apache.tika:tika-parser-microsoft-module:2.9.2 - https://tika.apache.org/tika-parser-microsoft-module/) - * Apache Tika miscellaneous office format parser module (org.apache.tika:tika-parser-miscoffice-module:2.9.2 - https://tika.apache.org/tika-parser-miscoffice-module/) - * Apache Tika news parser module (org.apache.tika:tika-parser-news-module:2.9.2 - https://tika.apache.org/tika-parser-news-module/) - * Apache Tika OCR parser module (org.apache.tika:tika-parser-ocr-module:2.9.2 - https://tika.apache.org/tika-parser-ocr-module/) - * Apache Tika PDF parser module (org.apache.tika:tika-parser-pdf-module:2.9.2 - https://tika.apache.org/tika-parser-pdf-module/) - * Apache Tika package parser module (org.apache.tika:tika-parser-pkg-module:2.9.2 - https://tika.apache.org/tika-parser-pkg-module/) - * Apache Tika text parser module (org.apache.tika:tika-parser-text-module:2.9.2 - https://tika.apache.org/tika-parser-text-module/) - * Apache Tika WARC parser module (org.apache.tika:tika-parser-webarchive-module:2.9.2 - https://tika.apache.org/tika-parser-webarchive-module/) - * Apache Tika XML parser module (org.apache.tika:tika-parser-xml-module:2.9.2 - https://tika.apache.org/tika-parser-xml-module/) - * Apache Tika XMP commons (org.apache.tika:tika-parser-xmp-commons:2.9.2 - https://tika.apache.org/tika-parser-xmp-commons/) - * Apache Tika ZIP commons (org.apache.tika:tika-parser-zip-commons:2.9.2 - https://tika.apache.org/tika-parser-zip-commons/) - * Apache Tika standard parser package (org.apache.tika:tika-parsers-standard-package:2.9.2 - https://tika.apache.org/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/) - * tomcat-embed-core (org.apache.tomcat.embed:tomcat-embed-core:10.1.24 - https://tomcat.apache.org/) - * tomcat-embed-el (org.apache.tomcat.embed:tomcat-embed-el:10.1.24 - https://tomcat.apache.org/) - * tomcat-embed-websocket (org.apache.tomcat.embed:tomcat-embed-websocket:10.1.24 - https://tomcat.apache.org/) - * Apache Velocity - Engine (org.apache.velocity:velocity-engine-core:2.3 - http://velocity.apache.org/engine/devel/velocity-engine-core/) - * Apache Velocity - JSR 223 Scripting (org.apache.velocity:velocity-engine-scripting:2.2 - http://velocity.apache.org/engine/devel/velocity-engine-scripting/) + * Apache Thrift (org.apache.thrift:libthrift:0.19.0 - http://thrift.apache.org) + * Apache Tika core (org.apache.tika:tika-core:2.9.4 - https://tika.apache.org/) + * Apache Tika Apple parser module (org.apache.tika:tika-parser-apple-module:2.9.4 - https://tika.apache.org/tika-parser-apple-module/) + * Apache Tika audiovideo parser module (org.apache.tika:tika-parser-audiovideo-module:2.9.4 - https://tika.apache.org/tika-parser-audiovideo-module/) + * Apache Tika cad parser module (org.apache.tika:tika-parser-cad-module:2.9.4 - https://tika.apache.org/tika-parser-cad-module/) + * Apache Tika code parser module (org.apache.tika:tika-parser-code-module:2.9.4 - https://tika.apache.org/tika-parser-code-module/) + * Apache Tika crypto parser module (org.apache.tika:tika-parser-crypto-module:2.9.4 - https://tika.apache.org/tika-parser-crypto-module/) + * Apache Tika digest commons (org.apache.tika:tika-parser-digest-commons:2.9.4 - https://tika.apache.org/tika-parser-digest-commons/) + * Apache Tika font parser module (org.apache.tika:tika-parser-font-module:2.9.4 - https://tika.apache.org/tika-parser-font-module/) + * Apache Tika html parser module (org.apache.tika:tika-parser-html-module:2.9.4 - https://tika.apache.org/tika-parser-html-module/) + * Apache Tika image parser module (org.apache.tika:tika-parser-image-module:2.9.4 - https://tika.apache.org/tika-parser-image-module/) + * Apache Tika mail commons (org.apache.tika:tika-parser-mail-commons:2.9.4 - https://tika.apache.org/tika-parser-mail-commons/) + * Apache Tika mail parser module (org.apache.tika:tika-parser-mail-module:2.9.4 - https://tika.apache.org/tika-parser-mail-module/) + * Apache Tika Microsoft parser module (org.apache.tika:tika-parser-microsoft-module:2.9.4 - https://tika.apache.org/tika-parser-microsoft-module/) + * Apache Tika miscellaneous office format parser module (org.apache.tika:tika-parser-miscoffice-module:2.9.4 - https://tika.apache.org/tika-parser-miscoffice-module/) + * Apache Tika news parser module (org.apache.tika:tika-parser-news-module:2.9.4 - https://tika.apache.org/tika-parser-news-module/) + * Apache Tika OCR parser module (org.apache.tika:tika-parser-ocr-module:2.9.4 - https://tika.apache.org/tika-parser-ocr-module/) + * Apache Tika PDF parser module (org.apache.tika:tika-parser-pdf-module:2.9.4 - https://tika.apache.org/tika-parser-pdf-module/) + * Apache Tika package parser module (org.apache.tika:tika-parser-pkg-module:2.9.4 - https://tika.apache.org/tika-parser-pkg-module/) + * Apache Tika text parser module (org.apache.tika:tika-parser-text-module:2.9.4 - https://tika.apache.org/tika-parser-text-module/) + * Apache Tika WARC parser module (org.apache.tika:tika-parser-webarchive-module:2.9.4 - https://tika.apache.org/tika-parser-webarchive-module/) + * Apache Tika XML parser module (org.apache.tika:tika-parser-xml-module:2.9.4 - https://tika.apache.org/tika-parser-xml-module/) + * Apache Tika XMP commons (org.apache.tika:tika-parser-xmp-commons:2.9.4 - https://tika.apache.org/tika-parser-xmp-commons/) + * Apache Tika ZIP commons (org.apache.tika:tika-parser-zip-commons:2.9.4 - https://tika.apache.org/tika-parser-zip-commons/) + * Apache Tika standard parser package (org.apache.tika:tika-parsers-standard-package:2.9.4 - https://tika.apache.org/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/) + * tomcat-embed-core (org.apache.tomcat.embed:tomcat-embed-core:10.1.42 - https://tomcat.apache.org/) + * tomcat-embed-el (org.apache.tomcat.embed:tomcat-embed-el:10.1.42 - https://tomcat.apache.org/) + * tomcat-embed-websocket (org.apache.tomcat.embed:tomcat-embed-websocket:10.1.42 - https://tomcat.apache.org/) + * Apache Velocity - Engine (org.apache.velocity:velocity-engine-core:2.4.1 - http://velocity.apache.org/engine/devel/velocity-engine-core/) + * Apache Velocity - JSR 223 Scripting (org.apache.velocity:velocity-engine-scripting:2.3 - http://velocity.apache.org/engine/devel/velocity-engine-scripting/) + * Apache Velocity Tools - Generic tools (org.apache.velocity.tools:velocity-tools-generic:3.1 - https://velocity.apache.org/tools/devel/velocity-tools-generic/) * Axiom API (org.apache.ws.commons.axiom:axiom-api:1.2.14 - http://ws.apache.org/axiom/) * Axiom Impl (org.apache.ws.commons.axiom:axiom-impl:1.2.14 - http://ws.apache.org/axiom/) - * XmlBeans (org.apache.xmlbeans:xmlbeans:5.2.0 - https://xmlbeans.apache.org/) + * XmlBeans (org.apache.xmlbeans:xmlbeans:5.3.0 - https://xmlbeans.apache.org/) * Apache ZooKeeper - Server (org.apache.zookeeper:zookeeper:3.6.2 - http://zookeeper.apache.org/zookeeper) * Apache ZooKeeper - Jute (org.apache.zookeeper:zookeeper-jute:3.6.2 - http://zookeeper.apache.org/zookeeper-jute) * org.apiguardian:apiguardian-api (org.apiguardian:apiguardian-api:1.1.2 - https://github.com/apiguardian-team/apiguardian) - * AssertJ Core (org.assertj:assertj-core:3.24.2 - https://assertj.github.io/doc/#assertj-core) + * AssertJ Core (org.assertj:assertj-core:3.27.3 - https://assertj.github.io/doc/#assertj-core) * Evo Inflector (org.atteo:evo-inflector:1.3 - http://atteo.org/static/evo-inflector) - * Awaitility (org.awaitility:awaitility:4.2.1 - http://awaitility.org) + * attoparser (org.attoparser:attoparser:2.0.7.RELEASE - https://www.attoparser.org) + * Awaitility (org.awaitility:awaitility:4.2.2 - http://awaitility.org) * jose4j (org.bitbucket.b_c:jose4j:0.6.5 - https://bitbucket.org/b_c/jose4j/) * TagSoup (org.ccil.cowan.tagsoup:tagsoup:1.2.1 - http://home.ccil.org/~cowan/XML/tagsoup/) * Woodstox (org.codehaus.woodstox:wstx-asl:3.2.6 - http://woodstox.codehaus.org) @@ -335,124 +344,122 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Jetty :: Asynchronous HTTP Client (org.eclipse.jetty:jetty-client:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-client) * Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-continuation) - * Jetty :: Deployers (org.eclipse.jetty:jetty-deploy:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-deploy) - * Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-http) - * Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-io) + * Jetty :: Deployers (org.eclipse.jetty:jetty-deploy:9.4.57.v20241219 - https://jetty.org/jetty-deploy/) + * Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.57.v20241219 - https://jetty.org/jetty-http/) + * Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.57.v20241219 - https://jetty.org/jetty-io/) * Jetty :: JMX Management (org.eclipse.jetty:jetty-jmx:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-jmx) * Jetty :: JNDI Naming (org.eclipse.jetty:jetty-jndi:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Plus (org.eclipse.jetty:jetty-plus:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Rewrite Handler (org.eclipse.jetty:jetty-rewrite:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-rewrite) * Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-security) - * Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-security) - * Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-server) - * Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-servlet) + * Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.57.v20241219 - https://jetty.org/jetty-security/) + * Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.57.v20241219 - https://jetty.org/jetty-server/) + * Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.57.v20241219 - https://jetty.org/jetty-servlet/) * Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-servlets) - * Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-util) - * Jetty :: Utilities :: Ajax(JSON) (org.eclipse.jetty:jetty-util-ajax:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-util-ajax) - * Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-webapp) + * Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.57.v20241219 - https://jetty.org/jetty-util/) + * Jetty :: Utilities :: Ajax(JSON) (org.eclipse.jetty:jetty-util-ajax:9.4.57.v20241219 - https://jetty.org/jetty-util-ajax/) + * Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.57.v20241219 - https://jetty.org/jetty-webapp/) * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-xml) - * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-xml) + * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.57.v20241219 - https://jetty.org/jetty-xml/) * Jetty :: ALPN :: API (org.eclipse.jetty.alpn:alpn-api:1.1.3.v20160715 - http://www.eclipse.org/jetty/alpn-api) * Jetty :: HTTP2 :: Client (org.eclipse.jetty.http2:http2-client:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-client) - * Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:9.4.54.v20240208 - https://eclipse.org/jetty/http2-parent/http2-common) + * Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:9.4.57.v20241219 - https://jetty.org/http2-parent/http2-common/) * Jetty :: HTTP2 :: HPACK (org.eclipse.jetty.http2:http2-hpack:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-hpack) * Jetty :: HTTP2 :: HTTP Client Transport (org.eclipse.jetty.http2:http2-http-client-transport:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-http-client-transport) * Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:9.4.15.v20190215 - https://eclipse.org/jetty/http2-parent/http2-server) * Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-server) * Jetty :: Schemas (org.eclipse.jetty.toolchain:jetty-schemas:3.1.2 - https://eclipse.org/jetty/jetty-schemas) * Ehcache (org.ehcache:ehcache:3.10.8 - http://ehcache.org) - * flyway-core (org.flywaydb:flyway-core:10.10.0 - https://flywaydb.org/flyway-core) - * flyway-database-postgresql (org.flywaydb:flyway-database-postgresql:10.10.0 - https://flywaydb.org/flyway-database-postgresql) + * flyway-core (org.flywaydb:flyway-core:10.22.0 - https://flywaydb.org/flyway-core) + * flyway-database-postgresql (org.flywaydb:flyway-database-postgresql:10.22.0 - https://flywaydb.org/flyway-database-postgresql) * Ogg and Vorbis for Java, Core (org.gagravarr:vorbis-java-core:0.8 - https://github.com/Gagravarr/VorbisJava) * Apache Tika plugin for Ogg, Vorbis and FLAC (org.gagravarr:vorbis-java-tika:0.8 - https://github.com/Gagravarr/VorbisJava) - * jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) - * Hibernate Validator Engine (org.hibernate.validator:hibernate-validator:8.0.1.Final - http://hibernate.org/validator/hibernate-validator) - * Hibernate Validator Portable Extension (org.hibernate.validator:hibernate-validator-cdi:8.0.1.Final - http://hibernate.org/validator/hibernate-validator-cdi) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart) + * Hibernate Validator Engine (org.hibernate.validator:hibernate-validator:8.0.2.Final - http://hibernate.org/validator/hibernate-validator) + * Hibernate Validator Portable Extension (org.hibernate.validator:hibernate-validator-cdi:8.0.2.Final - http://hibernate.org/validator/hibernate-validator-cdi) * org.immutables.value-annotations (org.immutables:value-annotations:2.9.2 - http://immutables.org/value-annotations) * leveldb (org.iq80.leveldb:leveldb:0.12 - http://github.com/dain/leveldb/leveldb) * leveldb-api (org.iq80.leveldb:leveldb-api:0.12 - http://github.com/dain/leveldb/leveldb-api) - * Javassist (org.javassist:javassist:3.29.2-GA - http://www.javassist.org/) - * JBoss Logging 3 (org.jboss.logging:jboss-logging:3.4.3.Final - http://www.jboss.org) + * Javassist (org.javassist:javassist:3.30.2-GA - https://www.javassist.org/) + * JBoss Logging 3 (org.jboss.logging:jboss-logging:3.6.1.Final - http://www.jboss.org) * JDOM (org.jdom:jdom2:2.0.6.1 - http://www.jdom.org) - * jtwig-core (org.jtwig:jtwig-core:5.87.0.RELEASE - http://jtwig.org) - * jtwig-reflection (org.jtwig:jtwig-reflection:5.87.0.RELEASE - http://jtwig.org) - * jtwig-spring (org.jtwig:jtwig-spring:5.87.0.RELEASE - http://jtwig.org) - * jtwig-spring-boot-starter (org.jtwig:jtwig-spring-boot-starter:5.87.0.RELEASE - http://jtwig.org) - * jtwig-web (org.jtwig:jtwig-web:5.87.0.RELEASE - http://jtwig.org) + * IntelliJ IDEA Annotations (org.jetbrains:annotations:13.0 - http://www.jetbrains.org) + * Kotlin Stdlib (org.jetbrains.kotlin:kotlin-stdlib:1.8.21 - https://kotlinlang.org/) + * Kotlin Stdlib Common (org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21 - https://kotlinlang.org/) + * Kotlin Stdlib Jdk7 (org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.21 - https://kotlinlang.org/) + * Kotlin Stdlib Jdk8 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.21 - https://kotlinlang.org/) * Proj4J (org.locationtech.proj4j:proj4j:1.1.5 - https://github.com/locationtech/proj4j) * Spatial4J (org.locationtech.spatial4j:spatial4j:0.7 - https://projects.eclipse.org/projects/locationtech.spatial4j) - * MockServer Java Client (org.mock-server:mockserver-client-java:5.11.2 - http://www.mock-server.com) - * MockServer Core (org.mock-server:mockserver-core:5.11.2 - http://www.mock-server.com) - * MockServer JUnit 4 Integration (org.mock-server:mockserver-junit-rule:5.11.2 - http://www.mock-server.com) - * MockServer & Proxy Netty (org.mock-server:mockserver-netty:5.11.2 - http://www.mock-server.com) - * Jetty Server (org.mortbay.jetty:jetty:6.1.26 - http://www.eclipse.org/jetty/jetty-parent/project/modules/jetty) - * Jetty Servlet Tester (org.mortbay.jetty:jetty-servlet-tester:6.1.26 - http://www.eclipse.org/jetty/jetty-parent/project/jetty-servlet-tester) - * Jetty Utilities (org.mortbay.jetty:jetty-util:6.1.26 - http://www.eclipse.org/jetty/jetty-parent/project/jetty-util) - * Servlet Specification API (org.mortbay.jetty:servlet-api:2.5-20081211 - http://jetty.mortbay.org/servlet-api) - * jwarc (org.netpreserve:jwarc:0.29.0 - https://github.com/iipc/jwarc) + * MockServer Java Client (org.mock-server:mockserver-client-java:5.15.0 - https://www.mock-server.com) + * MockServer Core (org.mock-server:mockserver-core:5.15.0 - https://www.mock-server.com) + * MockServer JUnit 4 Integration (org.mock-server:mockserver-junit-rule:5.15.0 - https://www.mock-server.com) + * MockServer & Proxy Netty (org.mock-server:mockserver-netty:5.15.0 - https://www.mock-server.com) + * jwarc (org.netpreserve:jwarc:0.31.1 - https://github.com/iipc/jwarc) * Objenesis (org.objenesis:objenesis:3.2 - http://objenesis.org/objenesis) - * parboiled-core (org.parboiled:parboiled-core:1.3.1 - http://parboiled.org) - * parboiled-java (org.parboiled:parboiled-java:1.3.1 - http://parboiled.org) - * org.roaringbitmap:RoaringBitmap (org.roaringbitmap:RoaringBitmap:0.9.45 - https://github.com/RoaringBitmap/RoaringBitmap) - * org.roaringbitmap:shims (org.roaringbitmap:shims:0.9.45 - https://github.com/RoaringBitmap/RoaringBitmap) + * org.opentest4j:opentest4j (org.opentest4j:opentest4j:1.3.0 - https://github.com/ota4j-team/opentest4j) + * org.roaringbitmap:RoaringBitmap (org.roaringbitmap:RoaringBitmap:1.0.0 - https://github.com/RoaringBitmap/RoaringBitmap) * RRD4J (org.rrd4j:rrd4j:3.5 - https://github.com/rrd4j/rrd4j/) - * Scala Library (org.scala-lang:scala-library:2.13.11 - https://www.scala-lang.org/) + * Scala Library (org.scala-lang:scala-library:2.13.2 - https://www.scala-lang.org/) * Scala Compiler (org.scala-lang:scala-reflect:2.13.0 - https://www.scala-lang.org/) * scala-collection-compat (org.scala-lang.modules:scala-collection-compat_2.13:2.1.6 - http://www.scala-lang.org/) * scala-java8-compat (org.scala-lang.modules:scala-java8-compat_2.13:0.9.0 - http://www.scala-lang.org/) * scala-parser-combinators (org.scala-lang.modules:scala-parser-combinators_2.13:1.1.2 - http://www.scala-lang.org/) * scala-xml (org.scala-lang.modules:scala-xml_2.13:1.3.0 - http://www.scala-lang.org/) - * JSONassert (org.skyscreamer:jsonassert:1.5.1 - https://github.com/skyscreamer/JSONassert) - * JCL 1.2 implemented over SLF4J (org.slf4j:jcl-over-slf4j:2.0.11 - http://www.slf4j.org) - * Spring AOP (org.springframework:spring-aop:6.1.8 - https://github.com/spring-projects/spring-framework) - * Spring Beans (org.springframework:spring-beans:6.1.8 - https://github.com/spring-projects/spring-framework) - * Spring Context (org.springframework:spring-context:6.1.8 - https://github.com/spring-projects/spring-framework) - * Spring Context Support (org.springframework:spring-context-support:6.1.8 - https://github.com/spring-projects/spring-framework) - * Spring Core (org.springframework:spring-core:6.1.8 - https://github.com/spring-projects/spring-framework) - * Spring Expression Language (SpEL) (org.springframework:spring-expression:6.1.8 - https://github.com/spring-projects/spring-framework) - * Spring Commons Logging Bridge (org.springframework:spring-jcl:6.1.8 - https://github.com/spring-projects/spring-framework) - * Spring JDBC (org.springframework:spring-jdbc:6.1.8 - https://github.com/spring-projects/spring-framework) - * Spring Object/Relational Mapping (org.springframework:spring-orm:6.1.8 - https://github.com/spring-projects/spring-framework) - * Spring TestContext Framework (org.springframework:spring-test:6.1.8 - https://github.com/spring-projects/spring-framework) - * Spring Transaction (org.springframework:spring-tx:6.1.8 - https://github.com/spring-projects/spring-framework) - * Spring Web (org.springframework:spring-web:6.1.8 - https://github.com/spring-projects/spring-framework) - * Spring Web MVC (org.springframework:spring-webmvc:6.1.8 - https://github.com/spring-projects/spring-framework) - * spring-boot (org.springframework.boot:spring-boot:3.2.6 - https://spring.io/projects/spring-boot) - * spring-boot-actuator (org.springframework.boot:spring-boot-actuator:3.2.6 - https://spring.io/projects/spring-boot) - * spring-boot-actuator-autoconfigure (org.springframework.boot:spring-boot-actuator-autoconfigure:3.2.6 - https://spring.io/projects/spring-boot) - * spring-boot-autoconfigure (org.springframework.boot:spring-boot-autoconfigure:3.2.6 - https://spring.io/projects/spring-boot) - * Spring Boot Configuration Processor (org.springframework.boot:spring-boot-configuration-processor:2.0.0.RELEASE - https://projects.spring.io/spring-boot/#/spring-boot-parent/spring-boot-tools/spring-boot-configuration-processor) - * spring-boot-starter (org.springframework.boot:spring-boot-starter:3.2.6 - https://spring.io/projects/spring-boot) - * spring-boot-starter-actuator (org.springframework.boot:spring-boot-starter-actuator:3.2.6 - https://spring.io/projects/spring-boot) - * spring-boot-starter-aop (org.springframework.boot:spring-boot-starter-aop:3.2.6 - https://spring.io/projects/spring-boot) - * spring-boot-starter-cache (org.springframework.boot:spring-boot-starter-cache:3.2.6 - https://spring.io/projects/spring-boot) - * spring-boot-starter-data-rest (org.springframework.boot:spring-boot-starter-data-rest:3.2.6 - https://spring.io/projects/spring-boot) - * spring-boot-starter-json (org.springframework.boot:spring-boot-starter-json:3.2.6 - https://spring.io/projects/spring-boot) - * spring-boot-starter-log4j2 (org.springframework.boot:spring-boot-starter-log4j2:3.2.6 - https://spring.io/projects/spring-boot) - * spring-boot-starter-security (org.springframework.boot:spring-boot-starter-security:3.2.6 - https://spring.io/projects/spring-boot) - * spring-boot-starter-test (org.springframework.boot:spring-boot-starter-test:3.2.6 - https://spring.io/projects/spring-boot) - * spring-boot-starter-tomcat (org.springframework.boot:spring-boot-starter-tomcat:3.2.6 - https://spring.io/projects/spring-boot) - * spring-boot-starter-web (org.springframework.boot:spring-boot-starter-web:3.2.6 - https://spring.io/projects/spring-boot) - * spring-boot-test (org.springframework.boot:spring-boot-test:3.2.6 - https://spring.io/projects/spring-boot) - * spring-boot-test-autoconfigure (org.springframework.boot:spring-boot-test-autoconfigure:3.2.6 - https://spring.io/projects/spring-boot) - * Spring Data Core (org.springframework.data:spring-data-commons:3.2.6 - https://spring.io/projects/spring-data) - * Spring Data REST - Core (org.springframework.data:spring-data-rest-core:4.2.6 - https://www.spring.io/spring-data/spring-data-rest-parent/spring-data-rest-core) - * Spring Data REST - WebMVC (org.springframework.data:spring-data-rest-webmvc:4.2.6 - https://www.spring.io/spring-data/spring-data-rest-parent/spring-data-rest-webmvc) - * Spring HATEOAS (org.springframework.hateoas:spring-hateoas:2.2.2 - https://github.com/spring-projects/spring-hateoas) + * JSONassert (org.skyscreamer:jsonassert:1.5.3 - https://github.com/skyscreamer/JSONassert) + * JCL 1.2 implemented over SLF4J (org.slf4j:jcl-over-slf4j:2.0.17 - http://www.slf4j.org) + * Spring AOP (org.springframework:spring-aop:6.2.8 - https://github.com/spring-projects/spring-framework) + * Spring Beans (org.springframework:spring-beans:6.2.8 - https://github.com/spring-projects/spring-framework) + * Spring Context (org.springframework:spring-context:6.2.8 - https://github.com/spring-projects/spring-framework) + * Spring Context Support (org.springframework:spring-context-support:6.2.8 - https://github.com/spring-projects/spring-framework) + * Spring Core (org.springframework:spring-core:6.2.8 - https://github.com/spring-projects/spring-framework) + * Spring Expression Language (SpEL) (org.springframework:spring-expression:6.2.8 - https://github.com/spring-projects/spring-framework) + * Spring Commons Logging Bridge (org.springframework:spring-jcl:6.2.8 - https://github.com/spring-projects/spring-framework) + * Spring JDBC (org.springframework:spring-jdbc:6.2.8 - https://github.com/spring-projects/spring-framework) + * Spring Object/Relational Mapping (org.springframework:spring-orm:6.2.8 - https://github.com/spring-projects/spring-framework) + * Spring TestContext Framework (org.springframework:spring-test:6.2.8 - https://github.com/spring-projects/spring-framework) + * Spring Transaction (org.springframework:spring-tx:6.2.8 - https://github.com/spring-projects/spring-framework) + * Spring Web (org.springframework:spring-web:6.2.8 - https://github.com/spring-projects/spring-framework) + * Spring Web MVC (org.springframework:spring-webmvc:6.2.8 - https://github.com/spring-projects/spring-framework) + * spring-boot (org.springframework.boot:spring-boot:3.5.3 - https://spring.io/projects/spring-boot) + * spring-boot-actuator (org.springframework.boot:spring-boot-actuator:3.5.3 - https://spring.io/projects/spring-boot) + * spring-boot-actuator-autoconfigure (org.springframework.boot:spring-boot-actuator-autoconfigure:3.5.3 - https://spring.io/projects/spring-boot) + * spring-boot-autoconfigure (org.springframework.boot:spring-boot-autoconfigure:3.5.3 - https://spring.io/projects/spring-boot) + * spring-boot-starter (org.springframework.boot:spring-boot-starter:3.5.3 - https://spring.io/projects/spring-boot) + * spring-boot-starter-actuator (org.springframework.boot:spring-boot-starter-actuator:3.5.3 - https://spring.io/projects/spring-boot) + * spring-boot-starter-aop (org.springframework.boot:spring-boot-starter-aop:3.5.3 - https://spring.io/projects/spring-boot) + * spring-boot-starter-cache (org.springframework.boot:spring-boot-starter-cache:3.5.3 - https://spring.io/projects/spring-boot) + * spring-boot-starter-data-rest (org.springframework.boot:spring-boot-starter-data-rest:3.5.3 - https://spring.io/projects/spring-boot) + * spring-boot-starter-json (org.springframework.boot:spring-boot-starter-json:3.5.3 - https://spring.io/projects/spring-boot) + * spring-boot-starter-log4j2 (org.springframework.boot:spring-boot-starter-log4j2:3.5.3 - https://spring.io/projects/spring-boot) + * spring-boot-starter-security (org.springframework.boot:spring-boot-starter-security:3.5.3 - https://spring.io/projects/spring-boot) + * spring-boot-starter-test (org.springframework.boot:spring-boot-starter-test:3.5.3 - https://spring.io/projects/spring-boot) + * spring-boot-starter-thymeleaf (org.springframework.boot:spring-boot-starter-thymeleaf:3.5.3 - https://spring.io/projects/spring-boot) + * spring-boot-starter-tomcat (org.springframework.boot:spring-boot-starter-tomcat:3.5.3 - https://spring.io/projects/spring-boot) + * spring-boot-starter-web (org.springframework.boot:spring-boot-starter-web:3.5.3 - https://spring.io/projects/spring-boot) + * spring-boot-test (org.springframework.boot:spring-boot-test:3.5.3 - https://spring.io/projects/spring-boot) + * spring-boot-test-autoconfigure (org.springframework.boot:spring-boot-test-autoconfigure:3.5.3 - https://spring.io/projects/spring-boot) + * Spring Data Core (org.springframework.data:spring-data-commons:3.5.1 - https://spring.io/projects/spring-data) + * Spring Data REST - Core (org.springframework.data:spring-data-rest-core:4.5.1 - https://www.spring.io/spring-data/spring-data-rest-parent/spring-data-rest-core) + * Spring Data REST - WebMVC (org.springframework.data:spring-data-rest-webmvc:4.5.1 - https://www.spring.io/spring-data/spring-data-rest-parent/spring-data-rest-webmvc) + * Spring HATEOAS (org.springframework.hateoas:spring-hateoas:2.5.1 - https://github.com/spring-projects/spring-hateoas) * Spring Plugin - Core (org.springframework.plugin:spring-plugin-core:3.0.0 - https://github.com/spring-projects/spring-plugin/spring-plugin-core) - * spring-security-config (org.springframework.security:spring-security-config:6.2.4 - https://spring.io/projects/spring-security) - * spring-security-core (org.springframework.security:spring-security-core:6.2.4 - https://spring.io/projects/spring-security) - * spring-security-crypto (org.springframework.security:spring-security-crypto:6.2.4 - https://spring.io/projects/spring-security) - * spring-security-test (org.springframework.security:spring-security-test:6.2.4 - https://spring.io/projects/spring-security) - * spring-security-web (org.springframework.security:spring-security-web:6.2.4 - https://spring.io/projects/spring-security) + * spring-security-config (org.springframework.security:spring-security-config:6.5.1 - https://spring.io/projects/spring-security) + * spring-security-core (org.springframework.security:spring-security-core:6.5.1 - https://spring.io/projects/spring-security) + * spring-security-crypto (org.springframework.security:spring-security-crypto:6.5.1 - https://spring.io/projects/spring-security) + * spring-security-test (org.springframework.security:spring-security-test:6.5.1 - https://spring.io/projects/spring-security) + * spring-security-web (org.springframework.security:spring-security-web:6.5.1 - https://spring.io/projects/spring-security) + * thymeleaf (org.thymeleaf:thymeleaf:3.1.3.RELEASE - http://www.thymeleaf.org/thymeleaf-lib/thymeleaf) + * thymeleaf-spring6 (org.thymeleaf:thymeleaf-spring6:3.1.3.RELEASE - http://www.thymeleaf.org/thymeleaf-lib/thymeleaf-spring6) + * unbescape (org.unbescape:unbescape:1.1.6.RELEASE - http://www.unbescape.org) * snappy-java (org.xerial.snappy:snappy-java:1.1.10.1 - https://github.com/xerial/snappy-java) * xml-matchers (org.xmlmatchers:xml-matchers:0.10 - http://code.google.com/p/xml-matchers/) - * org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.10.0 - https://www.xmlunit.org/) - * org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.9.1 - https://www.xmlunit.org/) - * org.xmlunit:xmlunit-placeholders (org.xmlunit:xmlunit-placeholders:2.8.0 - https://www.xmlunit.org/xmlunit-placeholders/) - * SnakeYAML (org.yaml:snakeyaml:2.2 - https://bitbucket.org/snakeyaml/snakeyaml) - * software.amazon.ion:ion-java (software.amazon.ion:ion-java:1.0.2 - https://github.com/amznlabs/ion-java/) + * org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.10.2 - https://www.xmlunit.org/) + * org.xmlunit:xmlunit-placeholders (org.xmlunit:xmlunit-placeholders:2.9.1 - https://www.xmlunit.org/xmlunit-placeholders/) + * SnakeYAML (org.yaml:snakeyaml:2.4 - https://bitbucket.org/snakeyaml/snakeyaml) * Xerces2-j (xerces:xercesImpl:2.12.2 - https://xerces.apache.org/xerces2-j/) BSD License: @@ -463,27 +470,31 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * JSONLD Java :: Core (com.github.jsonld-java:jsonld-java:0.13.4 - http://github.com/jsonld-java/jsonld-java/jsonld-java/) * curvesapi (com.github.virtuald:curvesapi:1.08 - https://github.com/virtuald/curvesapi) * Protocol Buffers [Core] (com.google.protobuf:protobuf-java:3.15.0 - https://developers.google.com/protocol-buffers/protobuf-java/) - * Protocol Buffers [Core] (com.google.protobuf:protobuf-java:3.23.3 - https://developers.google.com/protocol-buffers/protobuf-java/) + * Protocol Buffers [Core] (com.google.protobuf:protobuf-java:3.24.3 - https://developers.google.com/protocol-buffers/protobuf-java/) * JZlib (com.jcraft:jzlib:1.1.3 - http://www.jcraft.com/jzlib/) - * dnsjava (dnsjava:dnsjava:2.1.9 - http://www.dnsjava.org) + * jmustache (com.samskivert:jmustache:1.15 - http://github.com/samskivert/jmustache) + * dnsjava (dnsjava:dnsjava:3.6.3 - https://github.com/dnsjava/dnsjava) * jaxen (jaxen:jaxen:2.0.0 - http://www.cafeconleche.org/jaxen/jaxen) - * ANTLR 4 Runtime (org.antlr:antlr4-runtime:4.13.1 - https://www.antlr.org/antlr4-runtime/) + * ANTLR 4 Runtime (org.antlr:antlr4-runtime:4.13.2 - https://www.antlr.org/antlr4-runtime/) * commons-compiler (org.codehaus.janino:commons-compiler:3.1.8 - http://janino-compiler.github.io/commons-compiler/) * janino (org.codehaus.janino:janino:3.1.8 - http://janino-compiler.github.io/janino/) * Stax2 API (org.codehaus.woodstox:stax2-api:4.2.1 - http://github.com/FasterXML/stax2-api) * Hamcrest Date (org.exparity:hamcrest-date:2.0.8 - https://github.com/exparity/hamcrest-date) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart) * Hamcrest (org.hamcrest:hamcrest:2.2 - http://hamcrest.org/JavaHamcrest/) * Hamcrest Core (org.hamcrest:hamcrest-core:2.2 - http://hamcrest.org/JavaHamcrest/) - * HdrHistogram (org.hdrhistogram:HdrHistogram:2.1.12 - http://hdrhistogram.github.io/HdrHistogram/) + * HdrHistogram (org.hdrhistogram:HdrHistogram:2.2.2 - http://hdrhistogram.github.io/HdrHistogram/) * JBibTeX (org.jbibtex:jbibtex:1.0.20 - http://www.jbibtex.org) * asm (org.ow2.asm:asm:8.0.1 - http://asm.ow2.io/) * asm-analysis (org.ow2.asm:asm-analysis:8.0.1 - http://asm.ow2.io/) * asm-commons (org.ow2.asm:asm-commons:8.0.1 - http://asm.ow2.io/) * asm-tree (org.ow2.asm:asm-tree:8.0.1 - http://asm.ow2.io/) - * asm-util (org.ow2.asm:asm-util:7.1 - http://asm.ow2.org/) - * PostgreSQL JDBC Driver (org.postgresql:postgresql:42.7.3 - https://jdbc.postgresql.org) + * PostgreSQL JDBC Driver (org.postgresql:postgresql:42.7.7 - https://jdbc.postgresql.org) * Reflections (org.reflections:reflections:0.9.12 - http://github.com/ronmamo/reflections) * JMatIO (org.tallison:jmatio:1.5 - https://github.com/tballison/jmatio) + * XZ for Java (org.tukaani:xz:1.10 - https://tukaani.org/xz/java.html) * XMLUnit for Java (xmlunit:xmlunit:1.3 - http://xmlunit.sourceforge.net/) CC0: @@ -497,7 +508,7 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Old JAXB Runtime (com.sun.xml.bind:jaxb-impl:2.3.1 - http://jaxb.java.net/jaxb-bundles/jaxb-impl) * Jakarta Annotations API (jakarta.annotation:jakarta.annotation-api:2.1.1 - https://projects.eclipse.org/projects/ee4j.ca) * Jakarta Mail API (jakarta.mail:jakarta.mail-api:2.1.3 - https://projects.eclipse.org/projects/ee4j/jakarta.mail-api) - * Jakarta Servlet (jakarta.servlet:jakarta.servlet-api:6.0.0 - https://projects.eclipse.org/projects/ee4j.servlet) + * Jakarta Servlet (jakarta.servlet:jakarta.servlet-api:6.1.0 - https://projects.eclipse.org/projects/ee4j.servlet) * jakarta.transaction API (jakarta.transaction:jakarta.transaction-api:2.0.1 - https://projects.eclipse.org/projects/ee4j.jta) * JavaBeans Activation Framework API jar (javax.activation:javax.activation-api:1.2.0 - http://java.net/all/javax.activation-api/) * javax.annotation API (javax.annotation:javax.annotation-api:1.3 - http://jcp.org/en/jsr/detail?id=250) @@ -506,13 +517,13 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * jaxb-api (javax.xml.bind:jaxb-api:2.3.1 - https://github.com/javaee/jaxb-spec/jaxb-api) * JHighlight (org.codelibs:jhighlight:1.1.0 - https://github.com/codelibs/jhighlight) * Angus Mail default provider (org.eclipse.angus:jakarta.mail:2.0.3 - http://eclipse-ee4j.github.io/angus-mail/jakarta.mail) - * HK2 API module (org.glassfish.hk2:hk2-api:3.0.5 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-api) - * ServiceLocator Default Implementation (org.glassfish.hk2:hk2-locator:3.0.5 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-locator) - * HK2 Implementation Utilities (org.glassfish.hk2:hk2-utils:3.0.5 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-utils) + * HK2 API module (org.glassfish.hk2:hk2-api:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-api) + * ServiceLocator Default Implementation (org.glassfish.hk2:hk2-locator:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-locator) + * HK2 Implementation Utilities (org.glassfish.hk2:hk2-utils:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-utils) * OSGi resource locator (org.glassfish.hk2:osgi-resource-locator:1.0.3 - https://projects.eclipse.org/projects/ee4j/osgi-resource-locator) - * aopalliance version 1.0 repackaged as a module (org.glassfish.hk2.external:aopalliance-repackaged:3.0.5 - https://github.com/eclipse-ee4j/glassfish-hk2/external/aopalliance-repackaged) - * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * aopalliance version 1.0 repackaged as a module (org.glassfish.hk2.external:aopalliance-repackaged:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/external/aopalliance-repackaged) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart) Cordra (Version 2) License Agreement: @@ -536,8 +547,8 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * JAXB Core (org.glassfish.jaxb:jaxb-core:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) * JAXB Runtime (org.glassfish.jaxb:jaxb-runtime:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) * TXW2 Runtime (org.glassfish.jaxb:txw2:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) - * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart) * MIME streaming extension (org.jvnet.mimepull:mimepull:1.9.15 - https://github.com/eclipse-ee4j/metro-mimepull) * org.locationtech.jts:jts-core (org.locationtech.jts:jts-core:1.19.0 - https://www.locationtech.org/projects/technology.jts/jts-modules/jts-core) @@ -546,16 +557,15 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines Eclipse Public License: * System Rules (com.github.stefanbirkner:system-rules:1.19.0 - http://stefanbirkner.github.io/system-rules/) - * H2 Database Engine (com.h2database:h2:2.2.224 - https://h2database.com) + * H2 Database Engine (com.h2database:h2:2.3.232 - https://h2database.com) * Jakarta Annotations API (jakarta.annotation:jakarta.annotation-api:2.1.1 - https://projects.eclipse.org/projects/ee4j.ca) - * Jakarta Expression Language API (jakarta.el:jakarta.el-api:5.0.1 - https://projects.eclipse.org/projects/ee4j.el) * Jakarta Mail API (jakarta.mail:jakarta.mail-api:2.1.3 - https://projects.eclipse.org/projects/ee4j/jakarta.mail-api) * Jakarta Persistence API (jakarta.persistence:jakarta.persistence-api:3.1.0 - https://github.com/eclipse-ee4j/jpa-api) - * Jakarta Servlet (jakarta.servlet:jakarta.servlet-api:6.0.0 - https://projects.eclipse.org/projects/ee4j.servlet) + * Jakarta Servlet (jakarta.servlet:jakarta.servlet-api:6.1.0 - https://projects.eclipse.org/projects/ee4j.servlet) * jakarta.transaction API (jakarta.transaction:jakarta.transaction-api:2.0.1 - https://projects.eclipse.org/projects/ee4j.jta) * Jakarta RESTful WS API (jakarta.ws.rs:jakarta.ws.rs-api:3.1.0 - https://github.com/eclipse-ee4j/jaxrs-api) * JUnit (junit:junit:4.13.2 - http://junit.org) - * AspectJ Weaver (org.aspectj:aspectjweaver:1.9.22 - https://www.eclipse.org/aspectj/) + * AspectJ Weaver (org.aspectj:aspectjweaver:1.9.24 - https://www.eclipse.org/aspectj/) * Angus Mail default provider (org.eclipse.angus:jakarta.mail:2.0.3 - http://eclipse-ee4j.github.io/angus-mail/jakarta.mail) * Jetty :: Apache JSP Implementation (org.eclipse.jetty:apache-jsp:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Apache :: JSTL module (org.eclipse.jetty:apache-jstl:9.4.15.v20190215 - http://tomcat.apache.org/taglibs/standard/) @@ -569,53 +579,62 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Jetty :: Asynchronous HTTP Client (org.eclipse.jetty:jetty-client:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-client) * Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-continuation) - * Jetty :: Deployers (org.eclipse.jetty:jetty-deploy:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-deploy) - * Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-http) - * Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-io) + * Jetty :: Deployers (org.eclipse.jetty:jetty-deploy:9.4.57.v20241219 - https://jetty.org/jetty-deploy/) + * Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.57.v20241219 - https://jetty.org/jetty-http/) + * Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.57.v20241219 - https://jetty.org/jetty-io/) * Jetty :: JMX Management (org.eclipse.jetty:jetty-jmx:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-jmx) * Jetty :: JNDI Naming (org.eclipse.jetty:jetty-jndi:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Plus (org.eclipse.jetty:jetty-plus:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Rewrite Handler (org.eclipse.jetty:jetty-rewrite:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-rewrite) * Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-security) - * Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-security) - * Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-server) - * Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-servlet) + * Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.57.v20241219 - https://jetty.org/jetty-security/) + * Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.57.v20241219 - https://jetty.org/jetty-server/) + * Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.57.v20241219 - https://jetty.org/jetty-servlet/) * Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-servlets) - * Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-util) - * Jetty :: Utilities :: Ajax(JSON) (org.eclipse.jetty:jetty-util-ajax:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-util-ajax) - * Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-webapp) + * Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.57.v20241219 - https://jetty.org/jetty-util/) + * Jetty :: Utilities :: Ajax(JSON) (org.eclipse.jetty:jetty-util-ajax:9.4.57.v20241219 - https://jetty.org/jetty-util-ajax/) + * Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.57.v20241219 - https://jetty.org/jetty-webapp/) * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-xml) - * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.54.v20240208 - https://eclipse.org/jetty/jetty-xml) + * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.57.v20241219 - https://jetty.org/jetty-xml/) * Jetty :: ALPN :: API (org.eclipse.jetty.alpn:alpn-api:1.1.3.v20160715 - http://www.eclipse.org/jetty/alpn-api) * Jetty :: HTTP2 :: Client (org.eclipse.jetty.http2:http2-client:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-client) - * Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:9.4.54.v20240208 - https://eclipse.org/jetty/http2-parent/http2-common) + * Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:9.4.57.v20241219 - https://jetty.org/http2-parent/http2-common/) * Jetty :: HTTP2 :: HPACK (org.eclipse.jetty.http2:http2-hpack:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-hpack) * Jetty :: HTTP2 :: HTTP Client Transport (org.eclipse.jetty.http2:http2-http-client-transport:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-http-client-transport) * Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:9.4.15.v20190215 - https://eclipse.org/jetty/http2-parent/http2-server) * Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-server) * Jetty :: Schemas (org.eclipse.jetty.toolchain:jetty-schemas:3.1.2 - https://eclipse.org/jetty/jetty-schemas) * JSON-P Default Provider (org.glassfish:jakarta.json:2.0.1 - https://github.com/eclipse-ee4j/jsonp) - * HK2 API module (org.glassfish.hk2:hk2-api:3.0.5 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-api) - * ServiceLocator Default Implementation (org.glassfish.hk2:hk2-locator:3.0.5 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-locator) - * HK2 Implementation Utilities (org.glassfish.hk2:hk2-utils:3.0.5 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-utils) + * HK2 API module (org.glassfish.hk2:hk2-api:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-api) + * ServiceLocator Default Implementation (org.glassfish.hk2:hk2-locator:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-locator) + * HK2 Implementation Utilities (org.glassfish.hk2:hk2-utils:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-utils) * OSGi resource locator (org.glassfish.hk2:osgi-resource-locator:1.0.3 - https://projects.eclipse.org/projects/ee4j/osgi-resource-locator) - * aopalliance version 1.0 repackaged as a module (org.glassfish.hk2.external:aopalliance-repackaged:3.0.5 - https://github.com/eclipse-ee4j/glassfish-hk2/external/aopalliance-repackaged) - * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * aopalliance version 1.0 repackaged as a module (org.glassfish.hk2.external:aopalliance-repackaged:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/external/aopalliance-repackaged) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart) + * JUnit Platform Commons (org.junit.platform:junit-platform-commons:1.11.4 - https://junit.org/junit5/) + * JUnit Platform Engine API (org.junit.platform:junit-platform-engine:1.11.4 - https://junit.org/junit5/) + * JUnit Vintage Engine (org.junit.vintage:junit-vintage-engine:5.11.4 - https://junit.org/junit5/) * org.locationtech.jts:jts-core (org.locationtech.jts:jts-core:1.19.0 - https://www.locationtech.org/projects/technology.jts/jts-modules/jts-core) * org.locationtech.jts.io:jts-io-common (org.locationtech.jts.io:jts-io-common:1.19.0 - https://www.locationtech.org/projects/technology.jts/jts-modules/jts-io/jts-io-common) - * Jetty Server (org.mortbay.jetty:jetty:6.1.26 - http://www.eclipse.org/jetty/jetty-parent/project/modules/jetty) - * Jetty Servlet Tester (org.mortbay.jetty:jetty-servlet-tester:6.1.26 - http://www.eclipse.org/jetty/jetty-parent/project/jetty-servlet-tester) - * Jetty Utilities (org.mortbay.jetty:jetty-util:6.1.26 - http://www.eclipse.org/jetty/jetty-parent/project/jetty-util) + + GENERAL PUBLIC LICENSE, version 3 (GPL-3.0): + + * juniversalchardet (com.github.albfernandez:juniversalchardet:2.5.0 - https://github.com/albfernandez/juniversalchardet) + + GNU LESSER GENERAL PUBLIC LICENSE, version 3 (LGPL-3.0): + + * juniversalchardet (com.github.albfernandez:juniversalchardet:2.5.0 - https://github.com/albfernandez/juniversalchardet) GNU Lesser General Public License (LGPL): * btf (com.github.java-json-tools:btf:1.3 - https://github.com/java-json-tools/btf) * jackson-coreutils (com.github.java-json-tools:jackson-coreutils:2.0 - https://github.com/java-json-tools/jackson-coreutils) * jackson-coreutils-equivalence (com.github.java-json-tools:jackson-coreutils-equivalence:1.0 - https://github.com/java-json-tools/jackson-coreutils) + * json-patch (com.github.java-json-tools:json-patch:1.13 - https://github.com/java-json-tools/json-patch) * json-schema-core (com.github.java-json-tools:json-schema-core:1.2.14 - https://github.com/java-json-tools/json-schema-core) * json-schema-validator (com.github.java-json-tools:json-schema-validator:2.2.14 - https://github.com/java-json-tools/json-schema-validator) * msg-simple (com.github.java-json-tools:msg-simple:1.2 - https://github.com/java-json-tools/msg-simple) @@ -627,16 +646,16 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Hibernate ORM - hibernate-jcache (org.hibernate.orm:hibernate-jcache:6.4.8.Final - https://hibernate.org/orm) * Hibernate ORM - hibernate-jpamodelgen (org.hibernate.orm:hibernate-jpamodelgen:6.4.8.Final - https://hibernate.org/orm) * im4java (org.im4java:im4java:1.4.0 - http://sourceforge.net/projects/im4java/) - * Javassist (org.javassist:javassist:3.29.2-GA - http://www.javassist.org/) + * Javassist (org.javassist:javassist:3.30.2-GA - https://www.javassist.org/) * XOM (xom:xom:1.3.9 - https://xom.nu) Go License: * RE2/J (com.google.re2j:re2j:1.2 - http://github.com/google/re2j) - Handle.Net Public License Agreement (Ver.2): + Handle.Net Public License Agreement (Ver.3): - * Handle Server (net.handle:handle:9.3.1 - https://www.handle.net) + * Handle Server (net.handle:handle:9.3.2 - https://www.handle.net) ISC License: @@ -648,25 +667,22 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * better-files (com.github.pathikrit:better-files_2.13:3.9.1 - https://github.com/pathikrit/better-files) * Java SemVer (com.github.zafarkhaja:java-semver:0.9.0 - https://github.com/zafarkhaja/jsemver) * dd-plist (com.googlecode.plist:dd-plist:1.28 - http://www.github.com/3breadt/dd-plist) - * DigitalCollections: IIIF API Library (de.digitalcollections.iiif:iiif-apis:0.3.10 - https://github.com/dbmdz/iiif-apis) + * DigitalCollections: IIIF API Library (de.digitalcollections.iiif:iiif-apis:0.3.11 - https://github.com/dbmdz/iiif-apis) * s3mock (io.findify:s3mock_2.13:0.2.6 - https://github.com/findify/s3mock) * ClassGraph (io.github.classgraph:classgraph:4.8.165 - https://github.com/classgraph/classgraph) * JOpt Simple (net.sf.jopt-simple:jopt-simple:5.0.4 - http://jopt-simple.github.io/jopt-simple) - * Bouncy Castle S/MIME API (org.bouncycastle:bcmail-jdk18on:1.77 - https://www.bouncycastle.org/java.html) - * Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs (org.bouncycastle:bcpkix-jdk15on:1.67 - http://www.bouncycastle.org/java.html) - * Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs (org.bouncycastle:bcpkix-jdk18on:1.78.1 - https://www.bouncycastle.org/java.html) - * Bouncy Castle Provider (org.bouncycastle:bcprov-jdk15on:1.67 - http://www.bouncycastle.org/java.html) - * Bouncy Castle Provider (org.bouncycastle:bcprov-jdk18on:1.78.1 - https://www.bouncycastle.org/java.html) - * Bouncy Castle ASN.1 Extension and Utility APIs (org.bouncycastle:bcutil-jdk18on:1.78.1 - https://www.bouncycastle.org/java.html) + * Bouncy Castle JavaMail S/MIME APIs (org.bouncycastle:bcmail-jdk18on:1.80 - https://www.bouncycastle.org/download/bouncy-castle-java/) + * Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs (org.bouncycastle:bcpkix-jdk18on:1.81 - https://www.bouncycastle.org/download/bouncy-castle-java/) + * Bouncy Castle Provider (org.bouncycastle:bcprov-jdk18on:1.81 - https://www.bouncycastle.org/download/bouncy-castle-java/) + * Bouncy Castle ASN.1 Extension and Utility APIs (org.bouncycastle:bcutil-jdk18on:1.81 - https://www.bouncycastle.org/download/bouncy-castle-java/) * org.brotli:dec (org.brotli:dec:0.1.2 - http://brotli.org/dec) - * Checker Qual (org.checkerframework:checker-qual:3.31.0 - https://checkerframework.org) - * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * Checker Qual (org.checkerframework:checker-qual:3.49.5 - https://checkerframework.org/) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart) * mockito-core (org.mockito:mockito-core:3.12.4 - https://github.com/mockito/mockito) * mockito-inline (org.mockito:mockito-inline:3.12.4 - https://github.com/mockito/mockito) - * SLF4J API Module (org.slf4j:slf4j-api:2.0.11 - http://www.slf4j.org) - * SLF4J Extensions Module (org.slf4j:slf4j-ext:1.7.28 - http://www.slf4j.org) + * SLF4J API Module (org.slf4j:slf4j-api:2.0.17 - http://www.slf4j.org) * HAL Browser (org.webjars:hal-browser:ad9b865 - http://webjars.org) * toastr (org.webjars.bowergithub.codeseven:toastr:2.1.4 - http://webjars.org) * backbone (org.webjars.bowergithub.jashkenas:backbone:1.4.1 - https://www.webjars.org) @@ -674,28 +690,27 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * jquery (org.webjars.bowergithub.jquery:jquery-dist:3.7.1 - https://www.webjars.org) * urijs (org.webjars.bowergithub.medialize:uri.js:1.19.11 - https://www.webjars.org) * bootstrap (org.webjars.bowergithub.twbs:bootstrap:4.6.2 - https://www.webjars.org) - * core-js (org.webjars.npm:core-js:3.37.1 - https://www.webjars.org) - * @json-editor/json-editor (org.webjars.npm:json-editor__json-editor:2.6.1 - https://www.webjars.org) + * core-js (org.webjars.npm:core-js:3.42.0 - https://www.webjars.org) + * @json-editor/json-editor (org.webjars.npm:json-editor__json-editor:2.15.2 - https://www.webjars.org) Mozilla Public License: - * juniversalchardet (com.github.albfernandez:juniversalchardet:2.4.0 - https://github.com/albfernandez/juniversalchardet) - * H2 Database Engine (com.h2database:h2:2.2.224 - https://h2database.com) - * Saxon-HE (net.sf.saxon:Saxon-HE:9.8.0-14 - http://www.saxonica.com/) - * Javassist (org.javassist:javassist:3.29.2-GA - http://www.javassist.org/) + * juniversalchardet (com.github.albfernandez:juniversalchardet:2.5.0 - https://github.com/albfernandez/juniversalchardet) + * H2 Database Engine (com.h2database:h2:2.3.232 - https://h2database.com) + * Saxon-HE (net.sf.saxon:Saxon-HE:9.9.1-8 - http://www.saxonica.com/) + * Javassist (org.javassist:javassist:3.30.2-GA - https://www.javassist.org/) * Mozilla Rhino (org.mozilla:rhino:1.7.7.2 - https://developer.mozilla.org/en/Rhino) Public Domain: - * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart) - * HdrHistogram (org.hdrhistogram:HdrHistogram:2.1.12 - http://hdrhistogram.github.io/HdrHistogram/) + * HdrHistogram (org.hdrhistogram:HdrHistogram:2.2.2 - http://hdrhistogram.github.io/HdrHistogram/) * JSON in Java (org.json:json:20231013 - https://github.com/douglascrockford/JSON-java) * LatencyUtils (org.latencyutils:LatencyUtils:2.0.3 - http://latencyutils.github.io/LatencyUtils/) * Reflections (org.reflections:reflections:0.9.12 - http://github.com/ronmamo/reflections) - * XZ for Java (org.tukaani:xz:1.9 - https://tukaani.org/xz/java.html) UnRar License: @@ -703,16 +718,16 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines Unicode/ICU License: - * ICU4J (com.ibm.icu:icu4j:62.1 - http://icu-project.org/) + * ICU4J (com.ibm.icu:icu4j:62.2 - http://icu-project.org/) W3C license: - * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart) jQuery license: - * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.5 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart) diff --git a/README.md b/README.md index af9158eff361..1d93abe49948 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ DSpace consists of both a Java-based backend and an Angular-based frontend. * The REST Contract is at https://github.com/DSpace/RestContract * Frontend (https://github.com/DSpace/dspace-angular/) is the User Interface built on the REST API -Prior versions of DSpace (v6.x and below) used two different UIs (XMLUI and JSPUI). Those UIs are no longer supported in v7 (and above). +Prior versions of DSpace (v6.x and below) used two different UIs (XMLUI and JSPUI). Those UIs are no longer supported in v7 and above. * A maintenance branch for older versions is still available, see `dspace-6_x` for 6.x maintenance. ## Downloads @@ -33,18 +33,18 @@ Prior versions of DSpace (v6.x and below) used two different UIs (XMLUI and JSPU Documentation for each release may be viewed online or downloaded via our [Documentation Wiki](https://wiki.lyrasis.org/display/DSDOC/). The latest DSpace Installation instructions are available at: -https://wiki.lyrasis.org/display/DSDOC7x/Installing+DSpace +https://wiki.lyrasis.org/display/DSDOC8x/Installing+DSpace Please be aware that, as a Java web application, DSpace requires a database (PostgreSQL) and a servlet container (usually Tomcat) in order to function. More information about these and all other prerequisites can be found in the Installation instructions above. -## Running DSpace 7 in Docker +## Running DSpace 8 in Docker NOTE: At this time, we do not have production-ready Docker images for DSpace. That said, we do have quick-start Docker Compose scripts for development or testing purposes. -See [Running DSpace 7 with Docker Compose](dspace/src/main/docker-compose/README.md) +See [Running DSpace 8 with Docker Compose](dspace/src/main/docker-compose/README.md) ## Contributing @@ -64,7 +64,7 @@ Great Q&A is also available under the [DSpace tag on Stackoverflow](http://stack Additional support options are at https://wiki.lyrasis.org/display/DSPACE/Support DSpace also has an active service provider network. If you'd rather hire a service provider to -install, upgrade, customize or host DSpace, then we recommend getting in touch with one of our +install, upgrade, customize, or host DSpace, then we recommend getting in touch with one of our [Registered Service Providers](http://www.dspace.org/service-providers). ## Issue Tracker @@ -112,7 +112,7 @@ run automatically by [GitHub Actions](https://github.com/DSpace/DSpace/actions?q ``` * How to run only tests of a specific DSpace module ``` - # Before you can run only one module's tests, other modules may need installing into your ~/.m2 + # Before you can run only one module's tests, other modules may need to be installed into your ~/.m2 cd [dspace-src] mvn clean install diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml index 77e27b8768ac..46bd9ca80d62 100644 --- a/checkstyle-suppressions.xml +++ b/checkstyle-suppressions.xml @@ -7,4 +7,5 @@ + diff --git a/checkstyle.xml b/checkstyle.xml index e0fa808d83cb..36d2b15bd89e 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -92,7 +92,7 @@ For more information on CheckStyle configurations below, see: http://checkstyle. - + @@ -136,5 +136,22 @@ For more information on CheckStyle configurations below, see: http://checkstyle. + + + + + + + + + + + + + + + + + diff --git a/docker-compose-cli.yml b/docker-compose-cli.yml index 91f89916d208..5d15845fa8df 100644 --- a/docker-compose-cli.yml +++ b/docker-compose-cli.yml @@ -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:-dspace-8_x}" container_name: dspace-cli build: context: . diff --git a/docker-compose.yml b/docker-compose.yml index 6a930a8d31ec..9177ff4bd977 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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:-dspace-8_x-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:-dspace-8_x}" 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:-dspace-8_x}" build: context: ./dspace/src/main/docker/dspace-solr/ # Provide path to Solr configs necessary to build Docker image diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index d2d2d123c350..882f0b037c73 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -12,7 +12,7 @@ org.dspace dspace-parent - 8.1-SNAPSHOT + 8.2 .. @@ -102,7 +102,7 @@ org.codehaus.mojo build-helper-maven-plugin - 3.4.0 + 3.6.1 validate @@ -116,7 +116,7 @@ org.codehaus.mojo buildnumber-maven-plugin - 3.2.0 + 3.2.1 UNKNOWN_REVISION @@ -177,7 +177,7 @@ org.codehaus.mojo jaxb2-maven-plugin - 3.1.0 + 3.3.0 workflow-curation @@ -341,6 +341,14 @@ org.apache.logging.log4j log4j-api + + org.apache.logging.log4j + log4j-core + + + org.apache.logging.log4j + log4j-slf4j2-impl + org.hibernate.orm hibernate-core @@ -388,6 +396,13 @@ org.springframework spring-orm + + + + org.springframework + spring-jcl + + @@ -406,6 +421,16 @@ org.mortbay.jasper apache-jsp + + + org.bouncycastle + bcpkix-jdk15on + + + org.bouncycastle + bcprov-jdk15on + @@ -475,10 +500,6 @@ jakarta.annotation jakarta.annotation-api - - jakarta.el - jakarta.el-api - jaxen jaxen @@ -623,7 +644,7 @@ dnsjava dnsjava - 2.1.9 + 3.6.3 @@ -667,28 +688,6 @@ ${flyway.version} - - - com.google.apis - google-api-services-analytics - - - com.google.api-client - google-api-client - - - com.google.http-client - google-http-client - - - com.google.http-client - google-http-client-jackson2 - - - com.google.oauth-client - google-oauth-client - - com.google.code.findbugs @@ -702,7 +701,6 @@ jakarta.inject jakarta.inject-api - 2.0.1 @@ -733,7 +731,7 @@ com.amazonaws aws-java-sdk-s3 - 1.12.261 + 1.12.785 + + org.yaml + snakeyaml + @@ -769,25 +772,27 @@ com.opencsv opencsv - 5.9 + 5.11.1 org.apache.velocity velocity-engine-core + 2.4.1 org.xmlunit xmlunit-core + 2.10.2 test org.apache.bcel bcel - 6.7.0 + 6.10.0 test @@ -814,7 +819,7 @@ org.mock-server mockserver-junit-rule - 5.11.2 + 5.15.0 test @@ -855,76 +860,11 @@ - - - - - - - io.netty - netty-buffer - 4.1.106.Final - - - io.netty - netty-transport - 4.1.106.Final - - - io.netty - netty-transport-native-unix-common - 4.1.106.Final - - - io.netty - netty-common - 4.1.106.Final - - - io.netty - netty-handler - 4.1.106.Final - - - io.netty - netty-codec - 4.1.106.Final - - - org.apache.velocity - velocity-engine-core - 2.3 - - - org.xmlunit - xmlunit-core - 2.10.0 - test - - - com.github.java-json-tools - json-schema-validator - 2.2.14 - - - jakarta.validation - jakarta.validation-api - 3.0.2 - - - io.swagger - swagger-core - 1.6.2 - - - org.scala-lang - scala-library - 2.13.11 + + com.squareup.okhttp3 + mockwebserver test - - - - + + diff --git a/dspace-api/src/main/java/org/dspace/access/status/DefaultAccessStatusHelper.java b/dspace-api/src/main/java/org/dspace/access/status/DefaultAccessStatusHelper.java index 5f0e6d8b259b..52cdec3517bc 100644 --- a/dspace-api/src/main/java/org/dspace/access/status/DefaultAccessStatusHelper.java +++ b/dspace-api/src/main/java/org/dspace/access/status/DefaultAccessStatusHelper.java @@ -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.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 @@ private Date retrieveShortestEmbargo(Context context, Bitstream bitstream) throw // 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) { diff --git a/dspace-api/src/main/java/org/dspace/administer/RegistryImporter.java b/dspace-api/src/main/java/org/dspace/administer/RegistryImporter.java index 27a653421312..c74e56bce890 100644 --- a/dspace-api/src/main/java/org/dspace/administer/RegistryImporter.java +++ b/dspace-api/src/main/java/org/dspace/administer/RegistryImporter.java @@ -10,7 +10,6 @@ import java.io.File; import java.io.IOException; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.xpath.XPath; @@ -18,6 +17,7 @@ import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; +import org.dspace.app.util.XMLUtils; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -49,8 +49,9 @@ private RegistryImporter() { } */ public static Document loadXML(String filename) throws IOException, ParserConfigurationException, SAXException { - DocumentBuilder builder = DocumentBuilderFactory.newInstance() - .newDocumentBuilder(); + // This XML builder will *not* disable external entities as XML + // registries are considered trusted content + DocumentBuilder builder = XMLUtils.getTrustedDocumentBuilder(); Document document = builder.parse(new File(filename)); diff --git a/dspace-api/src/main/java/org/dspace/administer/RegistryLoader.java b/dspace-api/src/main/java/org/dspace/administer/RegistryLoader.java index bbf320a0d5e5..8bb72e18521e 100644 --- a/dspace-api/src/main/java/org/dspace/administer/RegistryLoader.java +++ b/dspace-api/src/main/java/org/dspace/administer/RegistryLoader.java @@ -13,7 +13,6 @@ import java.util.ArrayList; import java.util.Arrays; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.xpath.XPath; @@ -21,7 +20,15 @@ import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.BitstreamFormat; import org.dspace.content.factory.ContentServiceFactory; @@ -41,7 +48,7 @@ *

* RegistryLoader -bitstream bitstream-formats.xml *

- * RegistryLoader -dc dc-types.xml + * RegistryLoader -metadata dc-types.xml * * @author Robert Tansley * @version $Revision$ @@ -50,7 +57,7 @@ public class RegistryLoader { /** * log4j category */ - private static Logger log = org.apache.logging.log4j.LogManager.getLogger(RegistryLoader.class); + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(RegistryLoader.class); protected static BitstreamFormatService bitstreamFormatService = ContentServiceFactory.getInstance() .getBitstreamFormatService(); @@ -67,48 +74,97 @@ private RegistryLoader() { } * @throws Exception if error */ public static void main(String[] argv) throws Exception { - String usage = "Usage: " + RegistryLoader.class.getName() - + " (-bitstream | -metadata) registry-file.xml"; - - Context context = null; + // Set up command-line options and parse arguments + CommandLineParser parser = new DefaultParser(); + Options options = createCommandLineOptions(); try { - context = new Context(); + CommandLine line = parser.parse(options, argv); + + // Check if help option was entered or no options provided + if (line.hasOption('h') || line.getOptions().length == 0) { + printHelp(options); + System.exit(0); + } + + Context context = new Context(); // Can't update registries anonymously, so we need to turn off // authorisation context.turnOffAuthorisationSystem(); - // Work out what we're loading - if (argv[0].equalsIgnoreCase("-bitstream")) { - RegistryLoader.loadBitstreamFormats(context, argv[1]); - } else if (argv[0].equalsIgnoreCase("-metadata")) { - // Call MetadataImporter, as it handles Metadata schema updates - MetadataImporter.loadRegistry(argv[1], true); - } else { - System.err.println(usage); + try { + // Work out what we're loading + if (line.hasOption('b')) { + String filename = line.getOptionValue('b'); + if (StringUtils.isEmpty(filename)) { + System.err.println("No file path provided for bitstream format registry"); + printHelp(options); + System.exit(1); + } + RegistryLoader.loadBitstreamFormats(context, filename); + } else if (line.hasOption('m')) { + String filename = line.getOptionValue('m'); + if (StringUtils.isEmpty(filename)) { + System.err.println("No file path provided for metadata registry"); + printHelp(options); + System.exit(1); + } + // Call MetadataImporter, as it handles Metadata schema updates + MetadataImporter.loadRegistry(filename, true); + } else { + System.err.println("No registry type specified"); + printHelp(options); + System.exit(1); + } + + // Commit changes and close Context + context.complete(); + System.exit(0); + } catch (Exception e) { + log.fatal(LogHelper.getHeader(context, "error_loading_registries", ""), e); + System.err.println("Error: \n - " + e.getMessage()); + System.exit(1); + } finally { + // Clean up our context, if it still exists & it was never completed + if (context != null && context.isValid()) { + context.abort(); + } } + } catch (ParseException e) { + System.err.println("Error parsing command-line arguments: " + e.getMessage()); + printHelp(options); + System.exit(1); + } + } - // Commit changes and close Context - context.complete(); + /** + * Create the command-line options + * @return the command-line options + */ + private static Options createCommandLineOptions() { + Options options = new Options(); - System.exit(0); - } catch (ArrayIndexOutOfBoundsException ae) { - System.err.println(usage); + options.addOption("b", "bitstream", true, "load bitstream format registry from specified file"); + options.addOption("m", "metadata", true, "load metadata registry from specified file"); + options.addOption("h", "help", false, "print this help message"); - System.exit(1); - } catch (Exception e) { - log.fatal(LogHelper.getHeader(context, "error_loading_registries", - ""), e); + return options; + } - System.err.println("Error: \n - " + e.getMessage()); - System.exit(1); - } finally { - // Clean up our context, if it still exists & it was never completed - if (context != null && context.isValid()) { - context.abort(); - } - } + /** + * Print the help message + * @param options the command-line options + */ + private static void printHelp(Options options) { + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("RegistryLoader", + "Load bitstream format or metadata registries into the database\n", + options, + "\nExamples:\n" + + " RegistryLoader -b bitstream-formats.xml\n" + + " RegistryLoader -m dc-types.xml", + true); } /** @@ -210,8 +266,9 @@ private static void loadFormat(Context context, Node node) */ private static Document loadXML(String filename) throws IOException, ParserConfigurationException, SAXException { - DocumentBuilder builder = DocumentBuilderFactory.newInstance() - .newDocumentBuilder(); + // This XML builder will *not* disable external entities as XML + // registries are considered trusted content + DocumentBuilder builder = XMLUtils.getTrustedDocumentBuilder(); return builder.parse(new File(filename)); } @@ -221,7 +278,7 @@ private static Document loadXML(String filename) throws IOException, * contains: *

* - * <foo><mimetype>application/pdf</mimetype></foo> + * application/pdf * * passing this the foo node and mimetype will * return application/pdf. @@ -262,10 +319,10 @@ private static String getElementData(Node parentElement, String childName) * document contains: *

* - * <foo> - * <bar>val1</bar> - * <bar>val2</bar> - * </foo> + * + * val1 + * val2 + * * * passing this the foo node and bar will * return val1 and val2. diff --git a/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java b/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java index 13a1b3b5bbf8..f2577a37b176 100644 --- a/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java +++ b/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java @@ -27,7 +27,6 @@ import java.util.List; import java.util.Map; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.xpath.XPath; @@ -43,6 +42,7 @@ import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.apache.commons.lang3.StringUtils; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; import org.dspace.content.Community; @@ -613,8 +613,8 @@ private static String validateCollections(NodeList collections, int level) */ private static org.w3c.dom.Document loadXML(InputStream input) throws IOException, ParserConfigurationException, SAXException { - DocumentBuilder builder = DocumentBuilderFactory.newInstance() - .newDocumentBuilder(); + // This builder factory does not disable external DTD, entities, etc. + DocumentBuilder builder = XMLUtils.getTrustedDocumentBuilder(); org.w3c.dom.Document document = builder.parse(input); @@ -802,7 +802,7 @@ private static Element[] handleCollections(Context context, // default the short description to the empty string collectionService.setMetadataSingleValue(context, collection, - MD_SHORT_DESCRIPTION, Item.ANY, " "); + MD_SHORT_DESCRIPTION, null, " "); // import the rest of the metadata for (Map.Entry entry : collectionMap.entrySet()) { diff --git a/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java index 7bef232f0450..30f68efaf3cb 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java @@ -416,7 +416,7 @@ private DiscoverQuery buildDiscoveryQuery(String query, int start, int limit) { discoverQuery.setQuery(query); discoverQuery.setStart(start); discoverQuery.setMaxResults(limit); - + discoverQuery.setSortField("search.resourceid", DiscoverQuery.SORT_ORDER.asc); return discoverQuery; } diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/DSpaceCSV.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/DSpaceCSV.java index cbc052b5573f..3533a2397b3d 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/DSpaceCSV.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/DSpaceCSV.java @@ -188,6 +188,15 @@ public DSpaceCSV(InputStream inputStream, Context c) throws Exception { // Verify that the heading is valid in the metadata registry String[] clean = element.split("\\["); String[] parts = clean[0].split("\\."); + // Check language if present, if it's ANY then throw an exception + if (clean.length > 1 && clean[1].equals(Item.ANY + "]")) { + throw new MetadataImportInvalidHeadingException("Language ANY (*) was found in the heading " + + "of the metadata value to import, " + + "this should never be the case", + MetadataImportInvalidHeadingException.ENTRY, + columnCounter); + + } if (parts.length < 2) { throw new MetadataImportInvalidHeadingException(element, @@ -223,6 +232,15 @@ public DSpaceCSV(InputStream inputStream, Context c) throws Exception { } } + // Verify there isn’t already a header that is the same; if it already exists, + // throw MetadataImportInvalidHeadingException + String header = authorityPrefix + element; + if (headings.contains(header)) { + throw new MetadataImportInvalidHeadingException("Duplicate heading found: " + header, + MetadataImportInvalidHeadingException.ENTRY, + columnCounter); + } + // Store the heading headings.add(authorityPrefix + element); } diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java index 027ad116a7e2..e4bbe335d63e 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java @@ -143,7 +143,7 @@ public void internalRun() throws Exception { Iterator itemIterator = searchService.iteratorSearch(context, dso, discoverQuery); handler.logDebug("creating dspacecsv"); - DSpaceCSV dSpaceCSV = metadataDSpaceCsvExportService.export(context, itemIterator, true); + DSpaceCSV dSpaceCSV = metadataDSpaceCsvExportService.export(context, itemIterator, true, handler); handler.logDebug("writing to file " + getFileNameOrExportFile()); handler.writeFilestream(context, getFileNameOrExportFile(), dSpaceCSV.getInputStream(), EXPORT_CSV); context.restoreAuthSystemState(); diff --git a/dspace-api/src/main/java/org/dspace/app/client/DSpaceHttpClientFactory.java b/dspace-api/src/main/java/org/dspace/app/client/DSpaceHttpClientFactory.java new file mode 100644 index 000000000000..59c8172f722c --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/client/DSpaceHttpClientFactory.java @@ -0,0 +1,152 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.client; + +import static org.apache.commons.collections4.ListUtils.emptyIfNull; + +import java.util.List; + +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.HttpResponseInterceptor; +import org.apache.http.client.HttpClient; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.dspace.services.ConfigurationService; +import org.dspace.utils.DSpace; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Factory of {@link HttpClient} with common configurations. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class DSpaceHttpClientFactory { + + @Autowired + private ConfigurationService configurationService; + + @Autowired + private DSpaceProxyRoutePlanner proxyRoutePlanner; + + @Autowired(required = false) + private List requestInterceptors; + + @Autowired(required = false) + private List responseInterceptors; + + /** + * Get an instance of {@link DSpaceHttpClientFactory} from the Spring context. + * @return the bean instance + */ + public static DSpaceHttpClientFactory getInstance() { + return new DSpace().getSingletonService(DSpaceHttpClientFactory.class); + } + + /** + * Build an instance of {@link HttpClient} setting the proxy if configured. + * + * @return the client + */ + public CloseableHttpClient build() { + return build(HttpClientBuilder.create(), true); + } + + /** + * return a Builder if an instance of {@link HttpClient} pre-setting the proxy if configured. + * + * @return the client + */ + public HttpClientBuilder builder(boolean setProxy) { + HttpClientBuilder clientBuilder = HttpClientBuilder.create(); + if (setProxy) { + clientBuilder.setRoutePlanner(proxyRoutePlanner); + } + getRequestInterceptors().forEach(clientBuilder::addInterceptorLast); + getResponseInterceptors().forEach(clientBuilder::addInterceptorLast); + return clientBuilder; + } + + /** + * Build an instance of {@link HttpClient} without setting the proxy, even if + * configured. + * + * @return the client + */ + public CloseableHttpClient buildWithoutProxy() { + return build(HttpClientBuilder.create(), false); + } + + /** + * Build an instance of {@link HttpClient} setting the proxy if configured, + * disabling automatic retries and setting the maximum total connection. + * + * @param maxConnTotal the maximum total connection value + * @return the client + */ + public CloseableHttpClient buildWithoutAutomaticRetries(int maxConnTotal) { + HttpClientBuilder clientBuilder = HttpClientBuilder.create() + .disableAutomaticRetries() + .setMaxConnTotal(maxConnTotal); + return build(clientBuilder, true); + } + + /** + * Build an instance of {@link HttpClient} setting the proxy if configured with + * the given request configuration. + * @param requestConfig the request configuration + * @return the client + */ + public CloseableHttpClient buildWithRequestConfig(RequestConfig requestConfig) { + HttpClientBuilder httpClientBuilder = HttpClientBuilder.create() + .setDefaultRequestConfig(requestConfig); + return build(httpClientBuilder, true); + } + + private CloseableHttpClient build(HttpClientBuilder clientBuilder, boolean setProxy) { + if (setProxy) { + clientBuilder.setRoutePlanner(proxyRoutePlanner); + } + getRequestInterceptors().forEach(clientBuilder::addInterceptorLast); + getResponseInterceptors().forEach(clientBuilder::addInterceptorLast); + return clientBuilder.build(); + } + + public ConfigurationService getConfigurationService() { + return configurationService; + } + + public void setConfigurationService(ConfigurationService configurationService) { + this.configurationService = configurationService; + } + + public List getRequestInterceptors() { + return emptyIfNull(requestInterceptors); + } + + public void setRequestInterceptors(List requestInterceptors) { + this.requestInterceptors = requestInterceptors; + } + + public List getResponseInterceptors() { + return emptyIfNull(responseInterceptors); + } + + public void setResponseInterceptors(List responseInterceptors) { + this.responseInterceptors = responseInterceptors; + } + + public DSpaceProxyRoutePlanner getProxyRoutePlanner() { + return proxyRoutePlanner; + } + + public void setProxyRoutePlanner(DSpaceProxyRoutePlanner proxyRoutePlanner) { + this.proxyRoutePlanner = proxyRoutePlanner; + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/client/DSpaceProxyRoutePlanner.java b/dspace-api/src/main/java/org/dspace/app/client/DSpaceProxyRoutePlanner.java new file mode 100644 index 000000000000..1df7fa4a2985 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/client/DSpaceProxyRoutePlanner.java @@ -0,0 +1,73 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.client; + +import java.util.Arrays; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpException; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.impl.conn.DefaultRoutePlanner; +import org.apache.http.protocol.HttpContext; +import org.dspace.services.ConfigurationService; + +/** + * Extension of {@link DefaultRoutePlanner} that determine the proxy based on + * the configuration service, ignoring configured hosts. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class DSpaceProxyRoutePlanner extends DefaultRoutePlanner { + + private ConfigurationService configurationService; + + public DSpaceProxyRoutePlanner(ConfigurationService configurationService) { + super(null); + this.configurationService = configurationService; + } + + @Override + protected HttpHost determineProxy(HttpHost target, HttpRequest request, HttpContext context) throws HttpException { + if (isTargetHostConfiguredToBeIgnored(target)) { + return null; + } + String proxyHost = configurationService.getProperty("http.proxy.host"); + String proxyPort = configurationService.getProperty("http.proxy.port"); + if (StringUtils.isAnyBlank(proxyHost, proxyPort)) { + return null; + } + try { + return new HttpHost(proxyHost, Integer.parseInt(proxyPort), "http"); + } catch (NumberFormatException e) { + throw new RuntimeException("Invalid proxy port configuration: " + proxyPort); + } + } + + private boolean isTargetHostConfiguredToBeIgnored(HttpHost target) { + String[] hostsToIgnore = configurationService.getArrayProperty("http.proxy.hosts-to-ignore"); + if (ArrayUtils.isEmpty(hostsToIgnore)) { + return false; + } + return Arrays.stream(hostsToIgnore) + .anyMatch(host -> matchesHost(host, target.getHostName())); + } + + private boolean matchesHost(String hostPattern, String hostName) { + if (hostName.equals(hostPattern)) { + return true; + } else if (hostPattern.startsWith("*")) { + return hostName.endsWith(StringUtils.removeStart(hostPattern, "*")); + } else if (hostPattern.endsWith("*")) { + return hostName.startsWith(StringUtils.removeEnd(hostPattern, "*")); + } + return false; + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java index 087a33026151..7685ffc8f323 100644 --- a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java @@ -29,6 +29,7 @@ import java.io.OutputStream; import java.io.PrintWriter; import java.net.URL; +import java.nio.file.Path; import java.sql.SQLException; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -47,7 +48,6 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.xpath.XPath; @@ -67,6 +67,7 @@ import org.dspace.app.itemimport.service.ItemImportService; import org.dspace.app.util.LocalSchemaFilenameFilter; import org.dspace.app.util.RelationshipUtils; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.AuthorizeService; @@ -179,6 +180,8 @@ public class ItemImportServiceImpl implements ItemImportService, InitializingBea @Autowired(required = true) protected MetadataValueService metadataValueService; + protected DocumentBuilder builder; + protected String tempWorkDir; protected boolean isTest = false; @@ -742,15 +745,22 @@ protected Item addItem(Context c, List mycollections, String path, myitem = wi.getItem(); } + // normalize and validate path to make sure itemname doesn't contain path traversal + Path itemPath = new File(path + File.separatorChar + itemname + File.separatorChar) + .toPath().normalize(); + if (!itemPath.startsWith(path)) { + throw new IOException("Illegal item metadata path: '" + itemPath); + } + // Normalization chops off the last separator, and we need to put it back + String itemPathDir = itemPath.toString() + File.separatorChar; + // now fill out dublin core for item - loadMetadata(c, myitem, path + File.separatorChar + itemname - + File.separatorChar); + loadMetadata(c, myitem, itemPathDir); // and the bitstreams from the contents file // process contents file, add bistreams and bundles, return any // non-standard permissions - List options = processContentsFile(c, myitem, path - + File.separatorChar + itemname, "contents"); + List options = processContentsFile(c, myitem, itemPathDir, "contents"); if (useWorkflow) { // don't process handle file @@ -768,8 +778,7 @@ protected Item addItem(Context c, List mycollections, String path, } } else { // only process handle file if not using workflow system - String myhandle = processHandleFile(c, myitem, path - + File.separatorChar + itemname, "handle"); + String myhandle = processHandleFile(c, myitem, itemPathDir, "handle"); // put item in system if (!isTest) { @@ -1001,6 +1010,34 @@ protected void addDCValue(Context c, Item i, String schema, Node n) } } + /** + * Ensures a file path does not attempt to access files outside the designated parent directory. + * + * @param parentDir The absolute path to the parent directory that should contain the file + * @param fileName The name or path of the file to validate + * @throws IOException If an error occurs while resolving canonical paths, or the file path attempts + * to access a location outside the parent directory + */ + private void validateFilePath(String parentDir, String fileName) throws IOException { + File parent = new File(parentDir); + File file = new File(fileName); + + // If the fileName is not an absolute path, we resolve it against the parentDir + if (!file.isAbsolute()) { + file = new File(parent, fileName); + } + + String parentCanonicalPath = parent.getCanonicalPath(); + String fileCanonicalPath = file.getCanonicalPath(); + + if (!fileCanonicalPath.startsWith(parentCanonicalPath)) { + log.error("File path outside of canonical root requested: fileCanonicalPath={} does not begin " + + "with parentCanonicalPath={}", fileCanonicalPath, parentCanonicalPath); + throw new IOException("Illegal file path '" + fileName + "' encountered. This references a path " + + "outside of the import package. Please see the system logs for more details."); + } + } + /** * Read the collections file inside the item directory. If there * is one and it is not empty return a list of collections in @@ -1201,6 +1238,7 @@ protected List processContentsFile(Context c, Item i, String path, sDescription = sDescription.replaceFirst("description:", ""); } + validateFilePath(path, sFilePath); registerBitstream(c, i, iAssetstore, sFilePath, sBundle, sDescription); logInfo("\tRegistering Bitstream: " + sFilePath + "\tAssetstore: " + iAssetstore @@ -1414,6 +1452,7 @@ protected void processContentFileEntry(Context c, Item i, String path, return; } + validateFilePath(path, fileName); String fullpath = path + File.separatorChar + fileName; // get an input stream @@ -1888,9 +1927,7 @@ protected String getStringValue(Node node) { */ protected Document loadXML(String filename) throws IOException, ParserConfigurationException, SAXException { - DocumentBuilder builder = DocumentBuilderFactory.newInstance() - .newDocumentBuilder(); - + DocumentBuilder builder = XMLUtils.getDocumentBuilder(); return builder.parse(new File(filename)); } diff --git a/dspace-api/src/main/java/org/dspace/app/itemupdate/ItemArchive.java b/dspace-api/src/main/java/org/dspace/app/itemupdate/ItemArchive.java index 26de45caf77e..7dda65a0a75b 100644 --- a/dspace-api/src/main/java/org/dspace/app/itemupdate/ItemArchive.java +++ b/dspace-api/src/main/java/org/dspace/app/itemupdate/ItemArchive.java @@ -23,8 +23,6 @@ import java.util.Iterator; import java.util.List; import java.util.UUID; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; @@ -33,6 +31,7 @@ import org.apache.logging.log4j.Logger; import org.dspace.app.util.LocalSchemaFilenameFilter; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; @@ -52,7 +51,6 @@ public class ItemArchive { public static final String DUBLIN_CORE_XML = "dublin_core.xml"; - protected static DocumentBuilder builder = null; protected Transformer transformer = null; protected List dtomList = null; @@ -95,14 +93,14 @@ public static ItemArchive create(Context context, File dir, String itemField) InputStream is = null; try { is = new FileInputStream(new File(dir, DUBLIN_CORE_XML)); - itarch.dtomList = MetadataUtilities.loadDublinCore(getDocumentBuilder(), is); + itarch.dtomList = MetadataUtilities.loadDublinCore(XMLUtils.getDocumentBuilder(), is); //The code to search for local schema files was copied from org.dspace.app.itemimport // .ItemImportServiceImpl.java File file[] = dir.listFiles(new LocalSchemaFilenameFilter()); for (int i = 0; i < file.length; i++) { is = new FileInputStream(file[i]); - itarch.dtomList.addAll(MetadataUtilities.loadDublinCore(getDocumentBuilder(), is)); + itarch.dtomList.addAll(MetadataUtilities.loadDublinCore(XMLUtils.getDocumentBuilder(), is)); } } finally { if (is != null) { @@ -126,14 +124,6 @@ public static ItemArchive create(Context context, File dir, String itemField) return itarch; } - protected static DocumentBuilder getDocumentBuilder() - throws ParserConfigurationException { - if (builder == null) { - builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - } - return builder; - } - /** * Getter for Transformer * @@ -318,7 +308,7 @@ public void writeUndo(File undoDir) try { out = new FileOutputStream(new File(dir, "dublin_core.xml")); - Document doc = MetadataUtilities.writeDublinCore(getDocumentBuilder(), undoDtomList); + Document doc = MetadataUtilities.writeDublinCore(XMLUtils.getDocumentBuilder(), undoDtomList); MetadataUtilities.writeDocument(doc, getTransformer(), out); // if undo has delete bitstream diff --git a/dspace-api/src/main/java/org/dspace/app/launcher/ScriptLauncher.java b/dspace-api/src/main/java/org/dspace/app/launcher/ScriptLauncher.java index 89a416bfa883..ab8807c2cae3 100644 --- a/dspace-api/src/main/java/org/dspace/app/launcher/ScriptLauncher.java +++ b/dspace-api/src/main/java/org/dspace/app/launcher/ScriptLauncher.java @@ -19,6 +19,7 @@ import org.apache.commons.cli.ParseException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.core.Context; import org.dspace.scripts.DSpaceRunnable; import org.dspace.scripts.DSpaceRunnable.StepResult; @@ -314,7 +315,7 @@ public static Document getConfig(DSpaceKernelImpl kernelImpl) { String config = kernelImpl.getConfigurationService().getProperty("dspace.dir") + System.getProperty("file.separator") + "config" + System.getProperty("file.separator") + "launcher.xml"; - SAXBuilder saxBuilder = new SAXBuilder(); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document doc = null; try { doc = saxBuilder.build(config); diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageConsumer.java b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageConsumer.java index 210aaa6c9c97..8698559a5f89 100644 --- a/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageConsumer.java +++ b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageConsumer.java @@ -21,7 +21,6 @@ import java.util.UUID; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -48,6 +47,10 @@ import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.utils.DSpace; +import org.dspace.versioning.Version; +import org.dspace.versioning.VersionHistory; +import org.dspace.versioning.factory.VersionServiceFactory; +import org.dspace.versioning.service.VersionHistoryService; import org.dspace.web.ContextUtil; /** @@ -63,6 +66,9 @@ public class LDNMessageConsumer implements Consumer { private ConfigurationService configurationService; private ItemService itemService; private BitstreamService bitstreamService; + private final String RESUBMISSION_SUFFIX = "-resubmission"; + private final String ENDORSEMENT_PATTERN = "request-endorsement"; + private final String REVIEW_PATTERN = "request-review"; @Override public void initialize() throws Exception { @@ -83,6 +89,9 @@ public void consume(Context context, Event event) throws Exception { } Item item = (Item) event.getSubject(context); + if (item == null) { + return; + } createManualLDNMessages(context, item); createAutomaticLDNMessages(context, item); } @@ -90,10 +99,24 @@ public void consume(Context context, Event event) throws Exception { private void createManualLDNMessages(Context context, Item item) throws SQLException, JsonProcessingException { List patternsToTrigger = notifyPatternToTriggerService.findByItem(context, item); + // Note that multiple patterns can be submitted and not all support resubmission + // 1. Extract all patterns that accept resubmissions, i.e. endorsement and review + List patternsSupportingResubmission = patternsToTrigger.stream() + .filter(p -> p.getPattern().equals(REVIEW_PATTERN) || p.getPattern().equals(ENDORSEMENT_PATTERN)) + .map(NotifyPatternToTrigger::getID).toList(); + + String resubmissionReplyToID = null; for (NotifyPatternToTrigger patternToTrigger : patternsToTrigger) { + // Only try to fetch resubmission ID if the pattern support resubmission + if (patternsSupportingResubmission.contains(patternToTrigger.getID())) { + resubmissionReplyToID = findResubmissionReplyToUUID(context, item, patternToTrigger.getNotifyService()); + } + createLDNMessage(context,patternToTrigger.getItem(), - patternToTrigger.getNotifyService(), patternToTrigger.getPattern()); + patternToTrigger.getNotifyService(), + patternToTrigger.getPattern(), + resubmissionReplyToID); } } @@ -104,9 +127,31 @@ private void createAutomaticLDNMessages(Context context, Item item) throws SQLEx for (NotifyServiceInboundPattern inboundPattern : inboundPatterns) { if (StringUtils.isEmpty(inboundPattern.getConstraint()) || evaluateFilter(context, item, inboundPattern.getConstraint())) { - createLDNMessage(context, item, inboundPattern.getNotifyService(), inboundPattern.getPattern()); + createLDNMessage(context, item, inboundPattern.getNotifyService(), + inboundPattern.getPattern(), null); + } + } + } + + private String findResubmissionReplyToUUID(Context context, Item item, NotifyServiceEntity service) + throws SQLException { + // 1.1 Check whether this is a new version submission + VersionHistoryService versionHistoryService = VersionServiceFactory.getInstance() + .getVersionHistoryService(); + VersionHistory versionHistory = versionHistoryService.findByItem(context, item); + + if (versionHistory != null) { + Version currentVersion = versionHistoryService.getVersion(context, versionHistory, item); + Version previousVersion = versionHistoryService.getPrevious(context, versionHistory, currentVersion); + if (previousVersion != null) { + // 1.2 and a TentativeReject notification, matching the current pattern's service, was received for the + // previous item version + return ldnMessageService.findEndorsementOrReviewResubmissionIdByItem(context, + previousVersion.getItem(), service); } } + // New submission (new item, or previous version with a tentativeReject notification not found) + return null; } private boolean evaluateFilter(Context context, Item item, String constraint) { @@ -116,19 +161,40 @@ private boolean evaluateFilter(Context context, Item item, String constraint) { return filter != null && filter.getResult(context, item); } - private void createLDNMessage(Context context, Item item, NotifyServiceEntity service, String pattern) - throws SQLException, JsonMappingException, JsonProcessingException { - - LDN ldn = getLDNMessage(pattern); + private void createLDNMessage(Context context, Item item, NotifyServiceEntity service, String pattern, + String resubmissionID) + throws SQLException, JsonProcessingException { + // Amend current pattern name to trigger + // Endorsement or Review offer resubmissions: append '-resubmission' to pattern name to choose the correct + // LDN message template: e.g. request-endorsement-resubmission or request-review-resubmission + LDN ldn = (resubmissionID != null) + ? getLDNMessage(pattern + RESUBMISSION_SUFFIX) : getLDNMessage(pattern); LDNMessageEntity ldnMessage = - ldnMessageService.create(context, format("urn:uuid:%s", UUID.randomUUID())); + ldnMessageService.create(context, format("urn:uuid:%s", UUID.randomUUID())); ldnMessage.setObject(item); ldnMessage.setTarget(service); ldnMessage.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_QUEUED); ldnMessage.setQueueTimeout(new Date()); - appendGeneratedMessage(ldn, ldnMessage, pattern); + String actorID = null; + boolean serviceUsesActorEmailId = + configurationService.getBooleanProperty( + String.format("ldn.notification.supportsActorEmailId.%d", service.getID()), false); + if (serviceUsesActorEmailId) { + // If the service has been configured to use actorEmailId, we use the submitter's email and name + if (item.getSubmitter() != null) { + actorID = item.getSubmitter().getEmail(); + } else { + // Use configured fallback email (defaults to mail.admin property) + actorID = configurationService.getProperty("ldn.notification.email.submitter.fallback"); + } + } + appendGeneratedMessage(ldn, + ldnMessage, + actorID, + (actorID != null && item.getSubmitter() != null) ? item.getSubmitter().getFullName() : null, + resubmissionID); ObjectMapper mapper = new ObjectMapper(); Notification notification = mapper.readValue(ldnMessage.getMessage(), Notification.class); @@ -139,6 +205,10 @@ private void createLDNMessage(Context context, Item item, NotifyServiceEntity se Collections.sort(notificationTypeArrayList); ldnMessage.setActivityStreamType(notificationTypeArrayList.get(0)); ldnMessage.setCoarNotifyType(notificationTypeArrayList.get(1)); + // If a resubmission, set inReplyTo + if (resubmissionID != null) { + ldnMessage.setInReplyTo(ldnMessageService.find(context, resubmissionID)); + } ldnMessageService.update(context, ldnMessage); } @@ -151,11 +221,16 @@ private LDN getLDNMessage(String pattern) { } } - private void appendGeneratedMessage(LDN ldn, LDNMessageEntity ldnMessage, String pattern) { + private void appendGeneratedMessage(LDN ldn, LDNMessageEntity ldnMessage, String actorID, String actorName, + String resubmissionId) { Item item = (Item) ldnMessage.getObject(); - ldn.addArgument(getUiUrl()); + if (actorID != null) { + ldn.addArgument("mailto:" + actorID); + } else { + ldn.addArgument(getUiUrl()); + } ldn.addArgument(configurationService.getProperty("ldn.notify.inbox")); - ldn.addArgument(configurationService.getProperty("dspace.name")); + ldn.addArgument(actorName != null ? actorName : configurationService.getProperty("dspace.name")); ldn.addArgument(Objects.requireNonNullElse(ldnMessage.getTarget().getUrl(), "")); ldn.addArgument(Objects.requireNonNullElse(ldnMessage.getTarget().getLdnUrl(), "")); ldn.addArgument(getUiUrl() + "/handle/" + ldnMessage.getObject().getHandle()); @@ -166,6 +241,17 @@ private void appendGeneratedMessage(LDN ldn, LDNMessageEntity ldnMessage, String ldn.addArgument(getRelationUri(item)); ldn.addArgument("http://purl.org/vocab/frbr/core#supplement"); ldn.addArgument(format("urn:uuid:%s", UUID.randomUUID())); + if (actorID != null) { + ldn.addArgument("Person"); + } else { + ldn.addArgument("Service"); + } + // Param 14: UI URL, LDN message origin + ldn.addArgument(getUiUrl()); + // Param 15: inReplyTo ID, used in endorsement resubmission notifications + if (resubmissionId != null) { + ldn.addArgument(String.format("\"inReplyTo\": \"%s\",", resubmissionId)); + } ldnMessage.setMessage(ldn.generateLDNMessage()); } diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageEntity.java b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageEntity.java index 27257455e0ce..d57567ff1374 100644 --- a/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageEntity.java +++ b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageEntity.java @@ -20,6 +20,8 @@ import jakarta.persistence.TemporalType; import org.dspace.content.DSpaceObject; import org.dspace.core.ReloadableEntity; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; /** * Class representing ldnMessages stored in the DSpace system and, when locally resolvable, @@ -289,7 +291,11 @@ public String toString() { } public static String getNotificationType(LDNMessageEntity ldnMessage) { - if (ldnMessage.getInReplyTo() != null || ldnMessage.getOrigin() != null) { + // Resubmission outgoing notifications have the inReplyTo, therefore it cannot be used to determine + // whether a notification is incoming + ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + if (ldnMessage.getOrigin() != null && !ldnMessage.getOrigin().getLdnUrl() + .contains(configurationService.getProperty("dspace.ui.url"))) { return TYPE_INCOMING; } return TYPE_OUTGOING; diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNEmailAction.java b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNEmailAction.java index b87001f81500..d1eddb205b7a 100644 --- a/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNEmailAction.java +++ b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNEmailAction.java @@ -27,7 +27,7 @@ import org.springframework.beans.factory.annotation.Autowired; /** - * Action to send email to receipients provided in actionSendFilter. The email + * Action to send email to recipients provided in actionSendFilter. The email * body will be result of templating actionSendFilter. */ public class LDNEmailAction implements LDNAction { @@ -139,7 +139,13 @@ private List retrieveRecipientsEmail(Item item) { List recipients = new LinkedList(); if (actionSendFilter.startsWith("SUBMITTER")) { - recipients.add(item.getSubmitter().getEmail()); + if (item.getSubmitter() != null) { + recipients.add(item.getSubmitter().getEmail()); + } else { + // Fallback configured option + recipients.add(configurationService.getProperty("ldn.notification.email.submitter.fallback")); + } + } else if (actionSendFilter.startsWith("GROUP:")) { String groupName = actionSendFilter.replace("GROUP:", ""); String property = format("email.%s.list", groupName); diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/action/SendLDNMessageAction.java b/dspace-api/src/main/java/org/dspace/app/ldn/action/SendLDNMessageAction.java index c0ecf04304b8..c5a60144e63c 100644 --- a/dspace-api/src/main/java/org/dspace/app/ldn/action/SendLDNMessageAction.java +++ b/dspace-api/src/main/java/org/dspace/app/ldn/action/SendLDNMessageAction.java @@ -18,9 +18,9 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.app.ldn.model.Notification; import org.dspace.content.Item; import org.dspace.core.Context; @@ -34,21 +34,13 @@ public class SendLDNMessageAction implements LDNAction { private static final Logger log = LogManager.getLogger(SendLDNMessageAction.class); - private CloseableHttpClient client = null; + private CloseableHttpClient client; public SendLDNMessageAction() { - HttpClientBuilder builder = HttpClientBuilder.create(); - client = builder - .disableAutomaticRetries() - .setMaxConnTotal(5) - .build(); } public SendLDNMessageAction(CloseableHttpClient client) { - this(); - if (client != null) { - this.client = client; - } + this.client = client; } @Override @@ -66,9 +58,10 @@ public LDNActionStatus execute(Context context, Notification notification, Item // NOTE: Github believes there is a "Potential server-side request forgery due to a user-provided value" // This is a false positive because the LDN Service URL is configured by the user from DSpace. // See the frontend configuration at [dspace.ui.url]/admin/ldn/services - try ( - CloseableHttpResponse response = client.execute(httpPost); - ) { + if (client == null) { + client = DSpaceHttpClientFactory.getInstance().buildWithoutAutomaticRetries(5); + } + try (CloseableHttpResponse response = client.execute(httpPost)) { if (isSuccessful(response.getStatusLine().getStatusCode())) { result = LDNActionStatus.CONTINUE; } else if (isRedirect(response.getStatusLine().getStatusCode())) { @@ -77,6 +70,7 @@ public LDNActionStatus execute(Context context, Notification notification, Item } catch (Exception e) { log.error(e); } + client.close(); return result; } @@ -91,9 +85,9 @@ private boolean isRedirect(int statusCode) { statusCode == HttpStatus.SC_TEMPORARY_REDIRECT; } - private LDNActionStatus handleRedirect(CloseableHttpResponse oldresponse, + private LDNActionStatus handleRedirect(CloseableHttpResponse oldResponse, HttpPost request) throws HttpException { - Header[] urls = oldresponse.getHeaders(HttpHeaders.LOCATION); + Header[] urls = oldResponse.getHeaders(HttpHeaders.LOCATION); String url = urls.length > 0 && urls[0] != null ? urls[0].getValue() : null; if (url == null) { throw new HttpException("Error following redirect, unable to reach" @@ -102,17 +96,14 @@ private LDNActionStatus handleRedirect(CloseableHttpResponse oldresponse, LDNActionStatus result = LDNActionStatus.ABORT; try { request.setURI(new URI(url)); - try ( - CloseableHttpResponse response = client.execute(request); - ) { + try (CloseableHttpResponse response = client.execute(request)) { if (isSuccessful(response.getStatusLine().getStatusCode())) { - return LDNActionStatus.CONTINUE; + result = LDNActionStatus.CONTINUE; } } } catch (Exception e) { log.error("Error following redirect:", e); } - - return LDNActionStatus.ABORT; + return result; } } \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/LDNMessageDaoImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/LDNMessageDaoImpl.java index d811f6d39f34..4c935caf0d33 100644 --- a/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/LDNMessageDaoImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/LDNMessageDaoImpl.java @@ -149,8 +149,11 @@ public List findAllMessagesByItem( Predicate activityPredicate = null; andPredicates.add( criteriaBuilder.equal(root.get(LDNMessageEntity_.queueStatus), LDNMessageEntity.QUEUE_STATUS_PROCESSED)); + // Added OR with object or context - inbound notifications make use of the context item to provide information + // about the repository item the notification refers to andPredicates.add( - criteriaBuilder.equal(root.get(LDNMessageEntity_.object), item)); + criteriaBuilder.or(criteriaBuilder.equal(root.get(LDNMessageEntity_.object), item), + criteriaBuilder.equal(root.get(LDNMessageEntity_.context), item))); if (activities != null && activities.length > 0) { activityPredicate = root.get(LDNMessageEntity_.activityStreamType).in(activities); andPredicates.add(activityPredicate); diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/NotifyRequestStatus.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/NotifyRequestStatus.java index 0302b528aa8d..8333eae91024 100644 --- a/dspace-api/src/main/java/org/dspace/app/ldn/model/NotifyRequestStatus.java +++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/NotifyRequestStatus.java @@ -25,7 +25,7 @@ * "Offer", "coar-notify:IngestAction" * "Offer", "coar-notify:ReviewAction" * - * and their acknownledgements - if any + * and their acknowledgements - if any * * @author Francesco Bacchelli (francesco.bacchelli at 4science dot it) */ diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/NotifyRequestStatusEnum.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/NotifyRequestStatusEnum.java index 437c624f84d8..04265255711e 100644 --- a/dspace-api/src/main/java/org/dspace/app/ldn/model/NotifyRequestStatusEnum.java +++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/NotifyRequestStatusEnum.java @@ -8,11 +8,12 @@ package org.dspace.app.ldn.model; /** * REQUESTED means acknowledgements not received yet - * ACCEPTED means acknowledgements of "Accept" type received - * REJECTED means ack of "TentativeReject" type received + * ACCEPTED means acknowledgements of "Accept" or "TentativeAccept" type received + * REJECTED means ack of "Reject" type received + * TENTATIVE_REJECT means ack of "TentativeReject" type received * * @author Francesco Bacchelli (francesco.bacchelli at 4science.com) */ public enum NotifyRequestStatusEnum { - REJECTED, ACCEPTED, REQUESTED + REJECTED, ACCEPTED, REQUESTED, TENTATIVE_REJECT } diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/RequestStatus.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/RequestStatus.java index d19369830787..e33bc3eeb7d5 100644 --- a/dspace-api/src/main/java/org/dspace/app/ldn/model/RequestStatus.java +++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/RequestStatus.java @@ -8,7 +8,7 @@ package org.dspace.app.ldn.model; /** - * Informations about the Offer and Acknowledgements targeting a specified Item + * Information about the Offer and Acknowledgements targeting a specified Item * * @author Francesco Bacchelli (francesco.bacchelli at 4science.com) */ diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNMetadataProcessor.java b/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNMetadataProcessor.java index 43c50173ee61..c0bc89ab213f 100644 --- a/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNMetadataProcessor.java +++ b/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNMetadataProcessor.java @@ -20,6 +20,7 @@ import org.apache.http.client.HttpResponseException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.ldn.LDNMessageEntity; import org.dspace.app.ldn.action.LDNAction; import org.dspace.app.ldn.action.LDNActionStatus; import org.dspace.app.ldn.model.Notification; @@ -59,6 +60,8 @@ public class LDNMetadataProcessor implements LDNProcessor { "Announce", "TentativeReject", "Accept", + "TentativeAccept", + "Reject", "coar-notify:ReviewAction", "coar-notify:IngestAction", "coar-notify:EndorsementAction"); @@ -152,7 +155,7 @@ public void setActions(List actions) { } /** - * Lookup associated item to the notification context. If UUID in URL, lookup bu + * Lookup associated item to the notification context. If UUID in URL, lookup by * UUID, else lookup by handle. * * @param context current context @@ -168,7 +171,22 @@ private Item lookupItem(Context context, Notification notification) throws SQLEx String url = null; if (CONTEXT_ID_ITEM_TYPES.containsAll(notification.getType())) { - url = notification.getContext().getId(); + if (notification.getContext() != null) { + url = notification.getContext().getId(); + } else if (notification.getInReplyTo() != null) { + // Find context information (the item this notification relates to) via the inReplyTo notification ID + LDNMessageEntity inReplyToldnMessageEntity = + ldnMessageService.find(context, notification.getInReplyTo()); + if (inReplyToldnMessageEntity != null) { + String dspaceUrl = configurationService.getProperty("dspace.ui.url") + + "/handle/"; + url = dspaceUrl + inReplyToldnMessageEntity.getObject().getHandle(); + // Set context based on the inReplyTo and update in DB + LDNMessageEntity ldnMessageEntity = ldnMessageService.find(context, notification.getId()); + ldnMessageEntity.setContext(inReplyToldnMessageEntity.getObject()); + ldnMessageService.update(context, ldnMessageEntity); + } + } } else if (OBJECT_ID_ITEM_TYPES.containsAll(notification.getType())) { url = notification.getObject().getId(); } else if (OBJECT_SUBJECT_ITEM_TYPES.containsAll(notification.getType())) { diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/service/LDNMessageService.java b/dspace-api/src/main/java/org/dspace/app/ldn/service/LDNMessageService.java index eb18c6a69a70..01cb20626418 100644 --- a/dspace-api/src/main/java/org/dspace/app/ldn/service/LDNMessageService.java +++ b/dspace-api/src/main/java/org/dspace/app/ldn/service/LDNMessageService.java @@ -130,6 +130,17 @@ public interface LDNMessageService { */ public NotifyRequestStatus findRequestsByItem(Context context, Item item) throws SQLException; + /** + * find the UUID of a previous tentativeReject notification associated with a new resubmission (Endorsement or + * Review patterns only) + * + * @param context the context + * @param item the previousVersion item associated with a potential resubmission + * @return the UUID of a previous tentativeReject notification associated with a potential resubmission if found + */ + public String findEndorsementOrReviewResubmissionIdByItem(Context context, Item item, NotifyServiceEntity service) + throws SQLException; + /** * delete the provided ldn message * diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/LDNMessageServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/LDNMessageServiceImpl.java index 15f07a556112..1c101d512134 100644 --- a/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/LDNMessageServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/LDNMessageServiceImpl.java @@ -145,6 +145,12 @@ public LDNMessageEntity create(Context context, Notification notification, Strin ldnMessage.setActivityStreamType(notificationTypeArrayList.get(0)); if (notificationTypeArrayList.size() > 1) { ldnMessage.setCoarNotifyType(notificationTypeArrayList.get(1)); + } else { + // The Notification's Type array does not include the CoarNotifyType information, e.g. ack notifications + // Attempt to find it via the inReplyTo if present + if (ldnMessage.getInReplyTo() != null) { + ldnMessage.setCoarNotifyType(ldnMessage.getInReplyTo().getCoarNotifyType()); + } } ldnMessage.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_QUEUED); ldnMessage.setSourceIp(sourceIp); @@ -368,18 +374,23 @@ public NotifyRequestStatus findRequestsByItem(Context context, Item item) throws offer.setServiceUrl(nse == null ? "" : nse.getUrl()); offer.setOfferType(LDNUtils.getNotifyType(msg.getCoarNotifyType())); List acks = ldnMessageDao.findAllRelatedMessagesByItem( - context, msg, item, "Accept", "TentativeReject", "TentativeAccept", "Announce"); + context, msg, item, "Accept", "Reject", "TentativeReject", "TentativeAccept", + "Announce"); if (acks == null || acks.isEmpty()) { offer.setStatus(NotifyRequestStatusEnum.REQUESTED); + } else if (acks.stream() + .filter(c -> (c.getActivityStreamType().equalsIgnoreCase("TentativeReject"))) + .findAny().isPresent()) { + offer.setStatus(NotifyRequestStatusEnum.TENTATIVE_REJECT); + } else if (acks.stream() + .filter(c -> (c.getActivityStreamType().equalsIgnoreCase("Reject"))) + .findAny().isPresent()) { + offer.setStatus(NotifyRequestStatusEnum.REJECTED); } else if (acks.stream() .filter(c -> (c.getActivityStreamType().equalsIgnoreCase("TentativeAccept") || c.getActivityStreamType().equalsIgnoreCase("Accept"))) .findAny().isPresent()) { offer.setStatus(NotifyRequestStatusEnum.ACCEPTED); - } else if (acks.stream() - .filter(c -> c.getActivityStreamType().equalsIgnoreCase("TentativeReject")) - .findAny().isPresent()) { - offer.setStatus(NotifyRequestStatusEnum.REJECTED); } if (acks.stream().filter( c -> c.getActivityStreamType().equalsIgnoreCase("Announce")) @@ -391,6 +402,32 @@ public NotifyRequestStatus findRequestsByItem(Context context, Item item) throws return result; } + @Override + public String findEndorsementOrReviewResubmissionIdByItem(Context context, Item item, NotifyServiceEntity service) + throws SQLException { + List msgs = ldnMessageDao.findAllMessagesByItem( + context, item, "TentativeReject"); + + if (msgs != null && !msgs.isEmpty()) { + for (LDNMessageEntity msg : msgs) { + // Review and Endorsement are the only patterns supporting resubmissions at present + if (msg.getCoarNotifyType().contains("EndorsementAction") + || msg.getCoarNotifyType().contains("ReviewAction")) { + // Only provide the resubmissionReplyTo UUID if the pattern supports resubmission + // Add an extra check to ensure that this is a resubmission: current notification service + // matches the service associated with a previous tentativeReject response. This is to avoid a + // case where a previous version of the item received a tentativeReject from one service + // and the author decides to submit the version to a different service, instead of a resubmission + if (msg.getOrigin() != null && msg.getOrigin().getID().equals(service.getID())) { + // Return the first ID found that will be used in the inReplyTo for a resubmission notification + return msg.getID(); + } + } + } + } + return null; + } + public void delete(Context context, LDNMessageEntity ldnMessage) throws SQLException { ldnMessageDao.delete(context, ldnMessage); } diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterScript.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterScript.java index 5fbbebbb28cc..7f022f38b318 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterScript.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterScript.java @@ -7,6 +7,7 @@ */ package org.dspace.app.mediafilter; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -37,8 +38,9 @@ * MFM: -v verbose outputs all extracted text to STDOUT; -f force forces all * bitstreams to be processed, even if they have been before; -n noindex does not * recreate index after processing bitstreams; -i [identifier] limits processing - * scope to a community, collection or item; and -m [max] limits processing to a - * maximum number of items. + * scope to a community, collection or item; -m [max] limits processing to a + * maximum number of items; -fd [fromdate] takes only items starting from this date, + * filtering by last_modified in the item table. */ public class MediaFilterScript extends DSpaceRunnable { @@ -60,6 +62,7 @@ public class MediaFilterScript extends DSpaceRunnable> filterFormats = new HashMap<>(); + private LocalDate fromDate = null; public MediaFilterScriptConfiguration getScriptConfiguration() { return new DSpace().getServiceManager() @@ -112,6 +115,10 @@ public void setup() throws ParseException { skipIds = commandLine.getOptionValues('s'); } + if (commandLine.hasOption('d')) { + fromDate = LocalDate.parse(commandLine.getOptionValue('d')); + } + } @@ -215,6 +222,10 @@ public void internalRun() throws Exception { mediaFilterService.setSkipList(Arrays.asList(skipIds)); } + if (fromDate != null) { + mediaFilterService.setFromDate(fromDate); + } + Context c = null; try { diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterScriptConfiguration.java index 7465fa6e1279..c9f61292d617 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterScriptConfiguration.java @@ -52,6 +52,8 @@ public Options getOptions() { .build(); options.addOption(pluginOption); + options.addOption("d", "fromdate", true, "Process only item from specified last modified date"); + Option skipOption = Option.builder("s") .longOpt("skip") .hasArg() diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterServiceImpl.java index a6ba9fde88d9..512b8f803b9b 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterServiceImpl.java @@ -9,8 +9,11 @@ import java.io.InputStream; import java.sql.SQLException; +import java.time.LocalDate; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -93,6 +96,7 @@ public class MediaFilterServiceImpl implements MediaFilterService, InitializingB protected boolean isVerbose = false; protected boolean isQuiet = false; protected boolean isForce = false; // default to not forced + protected LocalDate fromDate = null; protected MediaFilterServiceImpl() { @@ -120,6 +124,15 @@ public void applyFiltersAllItems(Context context) throws Exception { for (Community topLevelCommunity : topLevelCommunities) { applyFiltersCommunity(context, topLevelCommunity); } + } else if (fromDate != null) { + Iterator itemIterator = + itemService.findByLastModifiedSince( + context, + Date.from(fromDate.atStartOfDay(ZoneId.systemDefault()).toInstant()) + ); + while (itemIterator.hasNext() && processed < max2Process) { + applyFiltersItem(context, itemIterator.next()); + } } else { //otherwise, just find every item and process Iterator itemIterator = itemService.findAll(context); @@ -588,4 +601,9 @@ public void setFilterFormats(Map> filterFormats) { public void setLogHandler(DSpaceRunnableHandler handler) { this.handler = handler; } + + @Override + public void setFromDate(LocalDate fromDate) { + this.fromDate = fromDate; + } } diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java index e83bf706ed02..5728f4f42f48 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java @@ -18,6 +18,7 @@ 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; @@ -37,6 +38,8 @@ public class TikaTextExtractionFilter extends MediaFilter { private final static Logger log = LogManager.getLogger(); + private static final int DEFAULT_MAX_CHARS = 100_000; + private static final int DEFAULT_MAX_ARRAY = 100_000_000; @Override public String getFilteredName(String oldFilename) { @@ -70,9 +73,12 @@ public InputStream getDestinationStream(Item currentItem, InputStream source, bo } // 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); + // Get maximum characters to extract. Default is 100,000 chars, which is also Tika's default setting. + int maxChars = configurationService.getIntProperty("textextractor.max-chars", DEFAULT_MAX_CHARS); + // Get maximum size of structure that Tika will try to buffer. + int maxArray = configurationService.getIntProperty("textextractor.max-array", DEFAULT_MAX_ARRAY); + IOUtils.setByteArrayMaxOverride(maxArray); try { // Use Tika to extract text from input. Tika will automatically detect the file type. Tika tika = new Tika(); @@ -80,13 +86,13 @@ public InputStream getDestinationStream(Item currentItem, InputStream source, bo 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; @@ -138,7 +144,7 @@ private InputStream extractUsingTempFile(InputStream source, boolean verbose) @Override public void characters(char[] ch, int start, int length) throws SAXException { try { - writer.append(new String(ch), start, length); + writer.append(new String(ch, start, length)); } catch (IOException e) { String errorMsg = String.format("Could not append to temporary file at %s " + "when performing text extraction", @@ -156,7 +162,7 @@ public void characters(char[] ch, int start, int length) throws SAXException { @Override public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { try { - writer.append(new String(ch), start, length); + writer.append(new String(ch, start, length)); } catch (IOException e) { String errorMsg = String.format("Could not append to temporary file at %s " + "when performing text extraction", @@ -167,6 +173,10 @@ public void ignorableWhitespace(char[] ch, int start, int length) throws SAXExce } }); + ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + int maxArray = configurationService.getIntProperty("textextractor.max-array", DEFAULT_MAX_ARRAY); + IOUtils.setByteArrayMaxOverride(maxArray); + AutoDetectParser parser = new AutoDetectParser(); Metadata metadata = new Metadata(); // parse our source InputStream using the above custom handler diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/service/MediaFilterService.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/service/MediaFilterService.java index bc92ff521098..30e6dba42f08 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/service/MediaFilterService.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/service/MediaFilterService.java @@ -8,6 +8,7 @@ package org.dspace.app.mediafilter.service; import java.sql.SQLException; +import java.time.LocalDate; import java.util.List; import java.util.Map; @@ -149,4 +150,6 @@ public void updatePoliciesOfDerivativeBitstreams(Context context, Item item, Bit * @param handler */ public void setLogHandler(DSpaceRunnableHandler handler); + + public void setFromDate(LocalDate fromDate); } diff --git a/dspace-api/src/main/java/org/dspace/app/sfx/SFXFileReaderServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/sfx/SFXFileReaderServiceImpl.java index 184f00a53e59..d3b447374a2c 100644 --- a/dspace-api/src/main/java/org/dspace/app/sfx/SFXFileReaderServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/sfx/SFXFileReaderServiceImpl.java @@ -18,6 +18,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.app.sfx.service.SFXFileReaderService; +import org.dspace.app.util.XMLUtils; import org.dspace.content.DCPersonName; import org.dspace.content.Item; import org.dspace.content.MetadataValue; @@ -79,9 +80,9 @@ public Document parseFile(String fileName) { log.info("Parsing XML file... " + fileName); DocumentBuilder docBuilder; Document doc = null; - DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); - docBuilderFactory.setIgnoringElementContentWhitespace(true); try { + DocumentBuilderFactory docBuilderFactory = XMLUtils.getDocumentBuilderFactory(); + docBuilderFactory.setIgnoringElementContentWhitespace(true); docBuilder = docBuilderFactory.newDocumentBuilder(); } catch (ParserConfigurationException e) { log.error("Wrong parser configuration: " + e.getMessage()); diff --git a/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAService.java b/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAService.java index 9ee5ca55cc6d..1fec20f51fee 100644 --- a/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAService.java +++ b/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAService.java @@ -17,15 +17,15 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.utils.URIBuilder; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.app.sherpa.v2.SHERPAPublisherResponse; import org.dspace.app.sherpa.v2.SHERPAResponse; import org.dspace.app.sherpa.v2.SHERPAUtils; @@ -45,8 +45,6 @@ */ public class SHERPAService { - private CloseableHttpClient client = null; - private int maxNumberOfTries; private long sleepBetweenTimeouts; private int timeout = 5000; @@ -59,26 +57,13 @@ public class SHERPAService { @Autowired ConfigurationService configurationService; - /** - * Create a new HTTP builder with sensible defaults in constructor - */ - public SHERPAService() { - HttpClientBuilder builder = HttpClientBuilder.create(); - // httpclient 4.3+ doesn't appear to have any sensible defaults any more. Setting conservative defaults as - // not to hammer the SHERPA service too much. - client = builder - .disableAutomaticRetries() - .setMaxConnTotal(5) - .build(); - } - /** * Complete initialization of the Bean. */ @SuppressWarnings("unused") @PostConstruct private void init() { - // Get endoint and API key from configuration + // Get endpoint and API key from configuration endpoint = configurationService.getProperty("sherpa.romeo.url", "https://v2.sherpa.ac.uk/cgi/retrieve"); apiKey = configurationService.getProperty("sherpa.romeo.apikey"); @@ -132,46 +117,47 @@ public SHERPAPublisherResponse performPublisherRequest(String type, String field timeout, sleepBetweenTimeouts)); - try { + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().buildWithoutAutomaticRetries(5)) { Thread.sleep(sleepBetweenTimeouts); // Construct a default HTTP method (first result) method = constructHttpGet(type, field, predicate, value, start, limit); // Execute the method - HttpResponse response = client.execute(method); - int statusCode = response.getStatusLine().getStatusCode(); + try (CloseableHttpResponse response = client.execute(method)) { + int statusCode = response.getStatusLine().getStatusCode(); - log.debug(response.getStatusLine().getStatusCode() + ": " - + response.getStatusLine().getReasonPhrase()); + log.debug(response.getStatusLine().getStatusCode() + ": " + + response.getStatusLine().getReasonPhrase()); - if (statusCode != HttpStatus.SC_OK) { - sherpaResponse = new SHERPAPublisherResponse("SHERPA/RoMEO return not OK status: " - + statusCode); - String errorBody = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); - log.error("Error from SHERPA HTTP request: " + errorBody); - } + if (statusCode != HttpStatus.SC_OK) { + sherpaResponse = new SHERPAPublisherResponse("SHERPA/RoMEO return not OK status: " + + statusCode); + String errorBody = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + log.error("Error from SHERPA HTTP request: " + errorBody); + } - HttpEntity responseBody = response.getEntity(); - - // If the response body is valid, pass to SHERPAResponse for parsing as JSON - if (null != responseBody) { - log.debug("Non-null SHERPA resonse received for query of " + value); - InputStream content = null; - try { - content = responseBody.getContent(); - sherpaResponse = - new SHERPAPublisherResponse(content, SHERPAPublisherResponse.SHERPAFormat.JSON); - } catch (IOException e) { - log.error("Encountered exception while contacting SHERPA/RoMEO: " + e.getMessage(), e); - } finally { - if (content != null) { - content.close(); + HttpEntity responseBody = response.getEntity(); + + // If the response body is valid, pass to SHERPAResponse for parsing as JSON + if (null != responseBody) { + log.debug("Non-null SHERPA response received for query of " + value); + InputStream content = null; + try { + content = responseBody.getContent(); + sherpaResponse = + new SHERPAPublisherResponse(content, SHERPAPublisherResponse.SHERPAFormat.JSON); + } catch (IOException e) { + log.error("Encountered exception while contacting SHERPA/RoMEO: " + e.getMessage(), e); + } finally { + if (content != null) { + content.close(); + } } + } else { + log.debug("Empty SHERPA response body for query on " + value); + sherpaResponse = new SHERPAPublisherResponse("SHERPA/RoMEO returned no response"); } - } else { - log.debug("Empty SHERPA response body for query on " + value); - sherpaResponse = new SHERPAPublisherResponse("SHERPA/RoMEO returned no response"); } } catch (URISyntaxException e) { String errorMessage = "Error building SHERPA v2 API URI: " + e.getMessage(); @@ -235,45 +221,46 @@ public SHERPAResponse performRequest(String type, String field, String predicate timeout, sleepBetweenTimeouts)); - try { + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().buildWithoutAutomaticRetries(5)) { Thread.sleep(sleepBetweenTimeouts); // Construct a default HTTP method (first result) method = constructHttpGet(type, field, predicate, value, start, limit); // Execute the method - HttpResponse response = client.execute(method); - int statusCode = response.getStatusLine().getStatusCode(); + try (CloseableHttpResponse response = client.execute(method)) { + int statusCode = response.getStatusLine().getStatusCode(); - log.debug(response.getStatusLine().getStatusCode() + ": " - + response.getStatusLine().getReasonPhrase()); + log.debug(response.getStatusLine().getStatusCode() + ": " + + response.getStatusLine().getReasonPhrase()); - if (statusCode != HttpStatus.SC_OK) { - sherpaResponse = new SHERPAResponse("SHERPA/RoMEO return not OK status: " - + statusCode); - String errorBody = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); - log.error("Error from SHERPA HTTP request: " + errorBody); - } + if (statusCode != HttpStatus.SC_OK) { + sherpaResponse = new SHERPAResponse("SHERPA/RoMEO return not OK status: " + + statusCode); + String errorBody = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + log.error("Error from SHERPA HTTP request: " + errorBody); + } - HttpEntity responseBody = response.getEntity(); - - // If the response body is valid, pass to SHERPAResponse for parsing as JSON - if (null != responseBody) { - log.debug("Non-null SHERPA resonse received for query of " + value); - InputStream content = null; - try { - content = responseBody.getContent(); - sherpaResponse = new SHERPAResponse(content, SHERPAResponse.SHERPAFormat.JSON); - } catch (IOException e) { - log.error("Encountered exception while contacting SHERPA/RoMEO: " + e.getMessage(), e); - } finally { - if (content != null) { - content.close(); + HttpEntity responseBody = response.getEntity(); + + // If the response body is valid, pass to SHERPAResponse for parsing as JSON + if (null != responseBody) { + log.debug("Non-null SHERPA response received for query of " + value); + InputStream content = null; + try { + content = responseBody.getContent(); + sherpaResponse = new SHERPAResponse(content, SHERPAResponse.SHERPAFormat.JSON); + } catch (IOException e) { + log.error("Encountered exception while contacting SHERPA/RoMEO: " + e.getMessage(), e); + } finally { + if (content != null) { + content.close(); + } } + } else { + log.debug("Empty SHERPA response body for query on " + value); + sherpaResponse = new SHERPAResponse("SHERPA/RoMEO returned no response"); } - } else { - log.debug("Empty SHERPA response body for query on " + value); - sherpaResponse = new SHERPAResponse("SHERPA/RoMEO returned no response"); } } catch (URISyntaxException e) { String errorMessage = "Error building SHERPA v2 API URI: " + e.getMessage(); @@ -283,7 +270,7 @@ public SHERPAResponse performRequest(String type, String field, String predicate String errorMessage = "Encountered exception while contacting SHERPA/RoMEO: " + e.getMessage(); log.error(errorMessage, e); sherpaResponse = new SHERPAResponse(errorMessage); - } catch (InterruptedException e) { + } catch (InterruptedException e) { String errorMessage = "Encountered exception while sleeping thread: " + e.getMessage(); log.error(errorMessage, e); sherpaResponse = new SHERPAResponse(errorMessage); diff --git a/dspace-api/src/main/java/org/dspace/app/sitemap/GenerateSitemaps.java b/dspace-api/src/main/java/org/dspace/app/sitemap/GenerateSitemaps.java index 90962d12aa75..41b0b0f6b3dd 100644 --- a/dspace-api/src/main/java/org/dspace/app/sitemap/GenerateSitemaps.java +++ b/dspace-api/src/main/java/org/dspace/app/sitemap/GenerateSitemaps.java @@ -7,6 +7,8 @@ */ package org.dspace.app.sitemap; +import static org.dspace.discovery.SearchUtils.RESOURCE_TYPE_FIELD; + import java.io.File; import java.io.IOException; import java.sql.SQLException; @@ -189,7 +191,8 @@ public static void generateSitemaps(boolean makeHTMLMap, boolean makeSitemapOrg) try { DiscoverQuery discoveryQuery = new DiscoverQuery(); discoveryQuery.setMaxResults(PAGE_SIZE); - discoveryQuery.setQuery("search.resourcetype:Community"); + discoveryQuery.setQuery("*:*"); + discoveryQuery.addFilterQueries(RESOURCE_TYPE_FIELD + ":Community"); do { discoveryQuery.setStart(offset); DiscoverResult discoverResult = searchService.search(c, discoveryQuery); @@ -213,7 +216,8 @@ public static void generateSitemaps(boolean makeHTMLMap, boolean makeSitemapOrg) offset = 0; discoveryQuery = new DiscoverQuery(); discoveryQuery.setMaxResults(PAGE_SIZE); - discoveryQuery.setQuery("search.resourcetype:Collection"); + discoveryQuery.setQuery("*:*"); + discoveryQuery.addFilterQueries(RESOURCE_TYPE_FIELD + ":Collection"); do { discoveryQuery.setStart(offset); DiscoverResult discoverResult = searchService.search(c, discoveryQuery); @@ -237,7 +241,8 @@ public static void generateSitemaps(boolean makeHTMLMap, boolean makeSitemapOrg) offset = 0; discoveryQuery = new DiscoverQuery(); discoveryQuery.setMaxResults(PAGE_SIZE); - discoveryQuery.setQuery("search.resourcetype:Item"); + discoveryQuery.setQuery("*:*"); + discoveryQuery.addFilterQueries(RESOURCE_TYPE_FIELD + ":Item"); discoveryQuery.addSearchField("search.entitytype"); do { diff --git a/dspace-api/src/main/java/org/dspace/app/solrdatabaseresync/SolrDatabaseResyncCli.java b/dspace-api/src/main/java/org/dspace/app/solrdatabaseresync/SolrDatabaseResyncCli.java index aac42ce1acf9..0ce6b1a9ef3d 100644 --- a/dspace-api/src/main/java/org/dspace/app/solrdatabaseresync/SolrDatabaseResyncCli.java +++ b/dspace-api/src/main/java/org/dspace/app/solrdatabaseresync/SolrDatabaseResyncCli.java @@ -98,7 +98,8 @@ public void internalRun() throws Exception { private void performStatusUpdate(Context context) throws SearchServiceException, SolrServerException, IOException { SolrQuery solrQuery = new SolrQuery(); - solrQuery.setQuery(STATUS_FIELD + ":" + STATUS_FIELD_PREDB); + solrQuery.setQuery("*:*"); + solrQuery.addFilterQuery(STATUS_FIELD + ":" + STATUS_FIELD_PREDB); solrQuery.addFilterQuery(SearchUtils.RESOURCE_TYPE_FIELD + ":" + IndexableItem.TYPE); String dateRangeFilter = SearchUtils.LAST_INDEXED_FIELD + ":[* TO " + maxTime + "]"; logDebugAndOut("Date range filter used; " + dateRangeFilter); diff --git a/dspace-api/src/main/java/org/dspace/app/statistics/LogAnalyser.java b/dspace-api/src/main/java/org/dspace/app/statistics/LogAnalyser.java index 2e4ed69b268e..c787261419f8 100644 --- a/dspace-api/src/main/java/org/dspace/app/statistics/LogAnalyser.java +++ b/dspace-api/src/main/java/org/dspace/app/statistics/LogAnalyser.java @@ -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 static String processLogs(Context context, String myLogDir, } // 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 static void setParameters(String myLogDir, String myFileTemplate, 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 static void setParameters(String myLogDir, String myFileTemplate, if (myConfigFile != null) { configFile = myConfigFile; - } else { - configFile = configurationService.getProperty("dspace.dir") - + File.separator + "config" + File.separator + "dstat.cfg"; } if (myStartDate != null) { diff --git a/dspace-api/src/main/java/org/dspace/app/statistics/package.html b/dspace-api/src/main/java/org/dspace/app/statistics/package.html index a6d8d8699cf7..931a7039080d 100644 --- a/dspace-api/src/main/java/org/dspace/app/statistics/package.html +++ b/dspace-api/src/main/java/org/dspace/app/statistics/package.html @@ -46,8 +46,6 @@

writes event records to the Java logger.
{@link org.dspace.statistics.SolrLoggerUsageEventListener SolrLoggerUsageEventListener}
writes event records to Solr.
-
{@link org.dspace.google.GoogleRecorderEventListener GoogleRecorderEventListener}<.dt> -
writes event records to Google Analytics.
diff --git a/dspace-api/src/main/java/org/dspace/app/util/DCInput.java b/dspace-api/src/main/java/org/dspace/app/util/DCInput.java index dd88390cb856..c96be33d0132 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/DCInput.java +++ b/dspace-api/src/main/java/org/dspace/app/util/DCInput.java @@ -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 @@ protected void initRegex(String regex) { /** * 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 diff --git a/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java b/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java index 8dc8239ca507..7c0ad4830f13 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java +++ b/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java @@ -15,7 +15,6 @@ import java.util.List; import java.util.Map; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.FactoryConfigurationError; import org.apache.commons.lang3.StringUtils; @@ -118,15 +117,17 @@ private void buildInputs(String fileName) formDefns = new HashMap>>>(); valuePairs = new HashMap>(); - String uri = "file:" + new File(fileName).getAbsolutePath(); + File inputFile = new File(fileName); + String inputFileDir = inputFile.toPath().normalize().getParent().toString(); - try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setValidating(false); - factory.setIgnoringComments(true); - factory.setIgnoringElementContentWhitespace(true); + String uri = "file:" + inputFile.getAbsolutePath(); - DocumentBuilder db = factory.newDocumentBuilder(); + try { + // This document builder will *not* disable external + // entities as they can be useful in managing large forms, but + // it will restrict them to be within the directory that the + // current input form XML file exists (or a sub-directory) + DocumentBuilder db = XMLUtils.getTrustedDocumentBuilder(inputFileDir); Document doc = db.parse(uri); doNodes(doc); checkValues(); @@ -379,7 +380,7 @@ private void processRow(String formName, int rowIdx, Node n, List 0) { leftCardinalityMin = node.getElementsByTagName(minOrMax).item(0).getTextContent(); } return leftCardinalityMin; } + /** + * Populate the relationship type based on values parsed from the XML relationship types configuration + * + * @param context DSpace context + * @param leftType left relationship type (e.g. "Publication"). + * @param rightType right relationship type (e.g. "Journal"). + * @param leftwardType leftward relationship type (e.g. "isAuthorOfPublication"). + * @param rightwardType rightward relationship type (e.g. "isPublicationOfAuthor"). + * @param leftCardinalityMin left cardinality min + * @param leftCardinalityMax left cardinality max + * @param rightCardinalityMin right cardinality min + * @param rightCardinalityMax right cardinality max + * @param copyToLeft copy metadata values to left if right side is deleted + * @param copyToRight copy metadata values to right if left side is deleted + * @param tilted set a tilted relationship side (left or right) if there are many relationships going one way + * to help performance (e.g. authors with 1000s of publications) + * @throws SQLException if database error occurs while saving the relationship type + * @throws AuthorizeException if authorization error occurs while saving the relationship type + */ private void populateRelationshipType(Context context, String leftType, String rightType, String leftwardType, String rightwardType, String leftCardinalityMin, String leftCardinalityMax, String rightCardinalityMin, String rightCardinalityMax, diff --git a/dspace-api/src/main/java/org/dspace/app/util/OpenSearchServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/util/OpenSearchServiceImpl.java index bff741b5ca42..2075ef7a3816 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/OpenSearchServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/util/OpenSearchServiceImpl.java @@ -101,6 +101,14 @@ protected String getBaseSearchUIURL() { configurationService.getProperty("websvc.opensearch.uicontext"); } + /** + * Get base search UI URL (websvc.opensearch.max_num_of_items_per_request) + */ + public int getMaxNumOfItemsPerRequest() { + return configurationService.getIntProperty( + "websvc.opensearch.max_num_of_items_per_request", 100); + } + @Override public String getContentType(String format) { return "html".equals(format) ? "text/html" : diff --git a/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java b/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java index 78be2bd4a41b..2016fb2ce64e 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java +++ b/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java @@ -16,7 +16,6 @@ import java.util.List; import java.util.Map; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.FactoryConfigurationError; import org.apache.commons.lang3.StringUtils; @@ -170,13 +169,10 @@ private void buildInputs(String fileName) throws SubmissionConfigReaderException String uri = "file:" + new File(fileName).getAbsolutePath(); try { - DocumentBuilderFactory factory = DocumentBuilderFactory - .newInstance(); - factory.setValidating(false); - factory.setIgnoringComments(true); - factory.setIgnoringElementContentWhitespace(true); - - DocumentBuilder db = factory.newDocumentBuilder(); + // This document builder factory will *not* disable external + // entities as they can be useful in managing large forms, but + // it will restrict them to the config dir containing submission definitions + DocumentBuilder db = XMLUtils.getTrustedDocumentBuilder(configDir); Document doc = db.parse(uri); doNodes(doc); } catch (FactoryConfigurationError fe) { @@ -732,4 +728,4 @@ public List getCollectionsBySubmissionConfig(Context context, String } return results; } -} \ No newline at end of file +} diff --git a/dspace-api/src/main/java/org/dspace/app/util/WebAppServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/util/WebAppServiceImpl.java index 8dcd78c8823b..fa23903ba431 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/WebAppServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/util/WebAppServiceImpl.java @@ -13,12 +13,12 @@ import java.util.Date; import java.util.List; -import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpHead; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.app.util.dao.WebAppDAO; import org.dspace.app.util.service.WebAppService; import org.dspace.core.Context; @@ -77,8 +77,8 @@ public List getApps() { for (WebApp app : webApps) { method = new HttpHead(app.getUrl()); int status; - try (CloseableHttpClient client = HttpClientBuilder.create().build()) { - HttpResponse response = client.execute(method); + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { + CloseableHttpResponse response = client.execute(method); status = response.getStatusLine().getStatusCode(); } if (status != HttpStatus.SC_OK) { diff --git a/dspace-api/src/main/java/org/dspace/app/util/XMLUtils.java b/dspace-api/src/main/java/org/dspace/app/util/XMLUtils.java index c39d0d26fd5e..6b419a0485e8 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/XMLUtils.java +++ b/dspace-api/src/main/java/org/dspace/app/util/XMLUtils.java @@ -7,12 +7,26 @@ */ package org.dspace.app.util; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.XMLInputFactory; import org.apache.commons.lang3.StringUtils; +import org.jdom2.input.SAXBuilder; import org.w3c.dom.Element; import org.w3c.dom.NodeList; +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; /** * Simple class to read information from small XML using DOM manipulation @@ -161,4 +175,195 @@ public static List getElementValueArrayList(Element rootElement, } return result; } + + /** + * Initialize and return a javax DocumentBuilderFactory with NO security + * applied. This is intended only for internal, administrative/configuration + * use where external entities and other dangerous features are actually + * purposefully included. + * The method here is tiny, but may be expanded with other features like + * whitespace handling, and calling this method name helps to document + * the fact that the caller knows it is trusting the XML source / factory. + * + * @return document builder factory to generate new builders + * @throws ParserConfigurationException + */ + public static DocumentBuilderFactory getTrustedDocumentBuilderFactory() + throws ParserConfigurationException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + return factory; + } + + /** + * Initialize and return the javax DocumentBuilderFactory with some basic security + * applied to avoid XXE attacks and other unwanted content inclusion + * @return document builder factory to generate new builders + * @throws ParserConfigurationException + */ + public static DocumentBuilderFactory getDocumentBuilderFactory() + throws ParserConfigurationException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + // No DOCTYPE / DTDs + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + // No external general entities + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + // No external parameter entities + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + // No external DTDs + factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + // Even if entities somehow get defined, they will not be expanded + factory.setExpandEntityReferences(false); + // Disable "XInclude" markup processing + factory.setXIncludeAware(false); + + return factory; + } + + /** + * Initialize and return a javax DocumentBuilder with less security + * applied. This is intended only for internal, administrative/configuration + * use where external entities and other dangerous features are actually + * purposefully included, but are only allowed from specified paths, e.g. + * dspace.dir or some other path specified by the java caller. + * The method here is tiny, but may be expanded with other features like + * whitespace handling, and calling this method name helps to document + * the fact that the caller knows it is trusting the XML source / builder + *

+ * If no allowedPaths are passed, then all external entities are rejected + * + * @return document builder with no security features set + * @throws ParserConfigurationException if the builder can not be configured + */ + public static DocumentBuilder getTrustedDocumentBuilder(String... allowedPaths) + throws ParserConfigurationException { + DocumentBuilderFactory factory = getTrustedDocumentBuilderFactory(); + factory.setValidating(false); + factory.setIgnoringComments(true); + factory.setIgnoringElementContentWhitespace(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + builder.setEntityResolver(new PathRestrictedEntityResolver(allowedPaths)); + return factory.newDocumentBuilder(); + } + + /** + * Initialize and return the javax DocumentBuilder with some basic security applied + * to avoid XXE attacks and other unwanted content inclusion + * @return document builder for use in XML parsing + * @throws ParserConfigurationException if the builder can not be configured + */ + public static DocumentBuilder getDocumentBuilder() + throws ParserConfigurationException { + return getDocumentBuilderFactory().newDocumentBuilder(); + } + + /** + * Initialize and return the SAX document builder with some basic security applied + * to avoid XXE attacks and other unwanted content inclusion + * @return SAX document builder for use in XML parsing + */ + public static SAXBuilder getSAXBuilder() { + return getSAXBuilder(false); + } + + /** + * Initialize and return the SAX document builder with some basic security applied + * to avoid XXE attacks and other unwanted content inclusion + * @param validate whether to use JDOM XSD validation + * @return SAX document builder for use in XML parsing + */ + public static SAXBuilder getSAXBuilder(boolean validate) { + SAXBuilder saxBuilder = new SAXBuilder(); + if (validate) { + saxBuilder.setValidation(true); + } + // No DOCTYPE / DTDs + saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + // No external general entities + saxBuilder.setFeature("http://xml.org/sax/features/external-general-entities", false); + // No external parameter entities + saxBuilder.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + // No external DTDs + saxBuilder.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + // Don't expand entities + saxBuilder.setExpandEntities(false); + + return saxBuilder; + } + + /** + * Initialize and return the Java XML Input Factory with some basic security applied + * to avoid XXE attacks and other unwanted content inclusion + * @return XML input factory for use in XML parsing + */ + public static XMLInputFactory getXMLInputFactory() { + XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); + xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); + + return xmlInputFactory; + } + + /** + * This entity resolver accepts one or more path strings in its + * constructor and throws a SAXException if the entity systemID + * is not within the allowed path (or a subdirectory). + * If no parameters are passed, then this effectively disallows + * any external entity resolution. + */ + public static class PathRestrictedEntityResolver implements EntityResolver { + private final List allowedBasePaths; + + public PathRestrictedEntityResolver(String... allowedBasePaths) { + this.allowedBasePaths = Arrays.asList(allowedBasePaths); + } + + @Override + public InputSource resolveEntity(String publicId, String systemId) + throws SAXException, IOException { + + if (systemId == null) { + return null; + } + + String filePath; + if (systemId.startsWith("file://")) { + filePath = systemId.substring(7); + } else if (systemId.startsWith("file:")) { + filePath = systemId.substring(5); + } else if (!systemId.contains("://")) { + filePath = systemId; + } else { + throw new SAXException("External resources not allowed: " + systemId + + ". Only local file paths are permitted."); + } + + Path resolvedPath; + try { + resolvedPath = Paths.get(filePath).toAbsolutePath().normalize(); + } catch (Exception e) { + throw new SAXException("Invalid path: " + systemId, e); + } + + boolean isAllowed = false; + for (String basePath : allowedBasePaths) { + Path allowedPath = Paths.get(basePath).toAbsolutePath().normalize(); + if (resolvedPath.startsWith(allowedPath)) { + isAllowed = true; + break; + } + } + + if (!isAllowed) { + throw new SAXException("Access denied to path: " + resolvedPath); + } + + File file = resolvedPath.toFile(); + if (!file.exists() || !file.canRead()) { + throw new SAXException("File not found or not readable: " + resolvedPath); + } + + return new InputSource(new FileInputStream(file)); + } + } + + } diff --git a/dspace-api/src/main/java/org/dspace/app/util/service/OpenSearchService.java b/dspace-api/src/main/java/org/dspace/app/util/service/OpenSearchService.java index 03f41e535c53..08900f8fff9c 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/service/OpenSearchService.java +++ b/dspace-api/src/main/java/org/dspace/app/util/service/OpenSearchService.java @@ -117,4 +117,10 @@ public Document getResultsDoc(Context context, String format, String query, int public DSpaceObject resolveScope(Context context, String scope) throws SQLException; + /** + * Retrieves the maximum number of items that can be included in a single opensearch request. + * + * @return the maximum number of items allowed per request + */ + int getMaxNumOfItemsPerRequest(); } diff --git a/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java b/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java index b791df15b5c0..40b8f48078c9 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java @@ -17,6 +17,7 @@ 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 @@ * @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 boolean allowSetPassword(Context context, return false; } - /* + /** * This is an explicit method. */ @Override @@ -138,7 +135,7 @@ public boolean isImplicit() { return false; } - /* + /** * Add authenticated users to the group defined in dspace.cfg by * the login.specialgroup key. */ @@ -177,7 +174,7 @@ public List getSpecialGroups(Context context, HttpServletRequest request) 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 List getSpecialGroups(Context context, HttpServletRequest request) * @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 int authenticate(Context context, } // 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 int authenticate(Context context, 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 int authenticate(Context context, 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 int authenticate(Context context, 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 int authenticate(Context context, return BAD_ARGS; } + /** + * Update eperson's attributes + */ + private void setEpersonAttributes(Context context, EPerson eperson, SpeakerToLDAP ldap, Optional 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 @@ protected String getDNOfUser(String adminUser, String adminPassword, Context con } else { searchName = ldap_provider_url + ldap_search_context; } + @SuppressWarnings("BanJNDI") NamingEnumeration answer = ctx.search( searchName, "(&({0}={1}))", new Object[] {ldap_id_field, @@ -553,7 +572,7 @@ protected String getDNOfUser(String adminUser, String adminPassword, Context con att = atts.get(attlist[4]); if (att != null) { // loop through all groups returned by LDAP - ldapGroup = new ArrayList(); + ldapGroup = new ArrayList<>(); for (NamingEnumeration val = att.getAll(); val.hasMoreElements(); ) { ldapGroup.add((String) val.next()); } @@ -633,7 +652,8 @@ protected boolean ldapAuthenticate(String netid, String password, 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 @@ protected boolean ldapAuthenticate(String netid, String password, } } - /* + /** * 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 String getName() { return "ldap"; } - /* + /** * Add authenticated users to the group defined in dspace.cfg by * the authentication-ldap.login.groupmap.* key. * diff --git a/dspace-api/src/main/java/org/dspace/authenticate/oidc/impl/OidcClientImpl.java b/dspace-api/src/main/java/org/dspace/authenticate/oidc/impl/OidcClientImpl.java index 68fffd3fb264..3e3d4b905b94 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/oidc/impl/OidcClientImpl.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/oidc/impl/OidcClientImpl.java @@ -22,12 +22,13 @@ import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; -import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.RequestBuilder; -import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.message.BasicNameValuePair; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.authenticate.oidc.OidcClient; import org.dspace.authenticate.oidc.OidcClientException; import org.dspace.authenticate.oidc.model.OidcTokenResponseDTO; @@ -83,21 +84,17 @@ public Map getUserInfo(String accessToken) throws OidcClientExce } private T executeAndParseJson(HttpUriRequest httpUriRequest, Class clazz) { - - HttpClient client = HttpClientBuilder.create().build(); - - return executeAndReturns(() -> { - - HttpResponse response = client.execute(httpUriRequest); - - if (isNotSuccessfull(response)) { - throw new OidcClientException(getStatusCode(response), formatErrorMessage(response)); - } - - return objectMapper.readValue(getContent(response), clazz); - - }); - + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { + return executeAndReturns(() -> { + CloseableHttpResponse response = client.execute(httpUriRequest); + if (isNotSuccessfull(response)) { + throw new OidcClientException(getStatusCode(response), formatErrorMessage(response)); + } + return objectMapper.readValue(getContent(response), clazz); + }); + } catch (IOException e) { + throw new RuntimeException(e); + } } private T executeAndReturns(ThrowingSupplier supplier) { diff --git a/dspace-api/src/main/java/org/dspace/authority/orcid/Orcidv3SolrAuthorityImpl.java b/dspace-api/src/main/java/org/dspace/authority/orcid/Orcidv3SolrAuthorityImpl.java index 6753a5d113b7..494daa97734a 100644 --- a/dspace-api/src/main/java/org/dspace/authority/orcid/Orcidv3SolrAuthorityImpl.java +++ b/dspace-api/src/main/java/org/dspace/authority/orcid/Orcidv3SolrAuthorityImpl.java @@ -7,27 +7,22 @@ */ package org.dspace.authority.orcid; -import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.net.URLEncoder; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import org.apache.commons.lang.StringUtils; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.authority.AuthorityValue; import org.dspace.authority.SolrAuthorityInterface; import org.dspace.external.OrcidRestConnector; import org.dspace.external.provider.orcid.xml.XMLtoBio; -import org.json.JSONObject; +import org.dspace.orcid.model.factory.OrcidFactoryUtils; import org.orcid.jaxb.model.v3.release.common.OrcidIdentifier; import org.orcid.jaxb.model.v3.release.record.Person; import org.orcid.jaxb.model.v3.release.search.Result; @@ -50,6 +45,11 @@ public class Orcidv3SolrAuthorityImpl implements SolrAuthorityInterface { private String accessToken; + /** + * Maximum retries to allow for the access token retrieval + */ + private int maxClientRetries = 3; + public void setOAUTHUrl(String oAUTHUrl) { OAUTHUrl = oAUTHUrl; } @@ -62,46 +62,32 @@ public void setClientSecret(String clientSecret) { this.clientSecret = clientSecret; } + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + /** * Initialize the accessToken that is required for all subsequent calls to ORCID */ public void init() { - if (StringUtils.isBlank(accessToken) - && StringUtils.isNotBlank(clientSecret) - && StringUtils.isNotBlank(clientId) - && StringUtils.isNotBlank(OAUTHUrl)) { - String authenticationParameters = "?client_id=" + clientId + - "&client_secret=" + clientSecret + - "&scope=/read-public&grant_type=client_credentials"; - try { - HttpPost httpPost = new HttpPost(OAUTHUrl + authenticationParameters); - httpPost.addHeader("Accept", "application/json"); - httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); - - HttpClient httpClient = HttpClientBuilder.create().build(); - HttpResponse getResponse = httpClient.execute(httpPost); - - JSONObject responseObject = null; - try (InputStream is = getResponse.getEntity().getContent(); - BufferedReader streamReader = new BufferedReader(new InputStreamReader(is, "UTF-8"))) { - String inputStr; - while ((inputStr = streamReader.readLine()) != null && responseObject == null) { - if (inputStr.startsWith("{") && inputStr.endsWith("}") && inputStr.contains("access_token")) { - try { - responseObject = new JSONObject(inputStr); - } catch (Exception e) { - //Not as valid as I'd hoped, move along - responseObject = null; - } - } - } - } - if (responseObject != null && responseObject.has("access_token")) { - accessToken = (String) responseObject.get("access_token"); - } - } catch (Exception e) { - throw new RuntimeException("Error during initialization of the Orcid connector", e); - } + // Initialize access token at spring instantiation. If it fails, the access token will be null rather + // than causing a fatal Spring startup error + initializeAccessToken(); + } + + public void initializeAccessToken() { + // If we have reaches max retries or the access token is already set, return immediately + if (maxClientRetries <= 0 || org.apache.commons.lang3.StringUtils.isNotBlank(accessToken)) { + return; + } + try { + accessToken = OrcidFactoryUtils.retrieveAccessToken(clientId, clientSecret, OAUTHUrl).orElse(null); + } catch (IOException e) { + log.error("Error retrieving ORCID access token, {} retries left", --maxClientRetries); } } @@ -116,7 +102,7 @@ public void setOrcidRestConnector(OrcidRestConnector orcidRestConnector) { */ @Override public List queryAuthorities(String text, int max) { - init(); + initializeAccessToken(); List bios = queryBio(text, max); List result = new ArrayList<>(); for (Person person : bios) { @@ -135,7 +121,7 @@ public List queryAuthorities(String text, int max) { */ @Override public AuthorityValue queryAuthorityID(String id) { - init(); + initializeAccessToken(); Person person = getBio(id); AuthorityValue valueFromPerson = Orcidv3AuthorityValue.create(person); return valueFromPerson; @@ -151,11 +137,14 @@ public Person getBio(String id) { if (!isValid(id)) { return null; } - init(); + if (orcidRestConnector == null) { + log.error("ORCID REST connector is null, returning null Person"); + return null; + } + initializeAccessToken(); InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), accessToken); XMLtoBio converter = new XMLtoBio(); - Person person = converter.convertSinglePerson(bioDocument); - return person; + return converter.convertSinglePerson(bioDocument); } @@ -167,10 +156,16 @@ public Person getBio(String id) { * @return List */ public List queryBio(String text, int start, int rows) { - init(); if (rows > 100) { throw new IllegalArgumentException("The maximum number of results to retrieve cannot exceed 100."); } + // Check REST connector is initialized + if (orcidRestConnector == null) { + log.error("ORCID REST connector is not initialized, returning empty list"); + return Collections.emptyList(); + } + // Check / init access token + initializeAccessToken(); String searchPath = "search?q=" + URLEncoder.encode(text) + "&start=" + start + "&rows=" + rows; log.debug("queryBio searchPath=" + searchPath + " accessToken=" + accessToken); diff --git a/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java b/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java index 932cd71744d4..f2692cf394fc 100644 --- a/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java @@ -9,6 +9,7 @@ import static org.dspace.app.util.AuthorizeUtil.canCollectionAdminManageAccounts; import static org.dspace.app.util.AuthorizeUtil.canCommunityAdminManageAccounts; +import static org.dspace.discovery.SearchUtils.RESOURCE_TYPE_FIELD; import java.sql.SQLException; import java.util.ArrayList; @@ -736,7 +737,7 @@ public List getPoliciesActionFilterExceptRpType(Context c, DSpac */ @Override public boolean isCommunityAdmin(Context context) throws SQLException { - return performCheck(context, "search.resourcetype:" + IndexableCommunity.TYPE); + return performCheck(context, RESOURCE_TYPE_FIELD + ":" + IndexableCommunity.TYPE); } /** @@ -749,7 +750,7 @@ public boolean isCommunityAdmin(Context context) throws SQLException { */ @Override public boolean isCollectionAdmin(Context context) throws SQLException { - return performCheck(context, "search.resourcetype:" + IndexableCollection.TYPE); + return performCheck(context, RESOURCE_TYPE_FIELD + ":" + IndexableCollection.TYPE); } /** @@ -762,7 +763,7 @@ public boolean isCollectionAdmin(Context context) throws SQLException { */ @Override public boolean isItemAdmin(Context context) throws SQLException { - return performCheck(context, "search.resourcetype:" + IndexableItem.TYPE); + return performCheck(context, RESOURCE_TYPE_FIELD + ":" + IndexableItem.TYPE); } /** @@ -776,8 +777,8 @@ public boolean isItemAdmin(Context context) throws SQLException { @Override public boolean isComColAdmin(Context context) throws SQLException { return performCheck(context, - "(search.resourcetype:" + IndexableCommunity.TYPE + " OR search.resourcetype:" + - IndexableCollection.TYPE + ")"); + "(" + RESOURCE_TYPE_FIELD + ":" + IndexableCommunity.TYPE + " OR " + + RESOURCE_TYPE_FIELD + ":" + IndexableCollection.TYPE + ")"); } /** @@ -795,7 +796,7 @@ public List findAdminAuthorizedCommunity(Context context, String quer throws SearchServiceException, SQLException { List communities = new ArrayList<>(); query = formatCustomQuery(query); - DiscoverResult discoverResult = getDiscoverResult(context, query + "search.resourcetype:" + + DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + IndexableCommunity.TYPE, offset, limit, null, null); for (IndexableObject solrCollections : discoverResult.getIndexableObjects()) { @@ -817,9 +818,9 @@ public List findAdminAuthorizedCommunity(Context context, String quer public long countAdminAuthorizedCommunity(Context context, String query) throws SearchServiceException, SQLException { query = formatCustomQuery(query); - DiscoverResult discoverResult = getDiscoverResult(context, query + "search.resourcetype:" + + DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + IndexableCommunity.TYPE, - null, null, null, null); + null, 0, null, null); return discoverResult.getTotalSearchResults(); } @@ -842,7 +843,7 @@ public List findAdminAuthorizedCollection(Context context, String qu } query = formatCustomQuery(query); - DiscoverResult discoverResult = getDiscoverResult(context, query + "search.resourcetype:" + + DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + IndexableCollection.TYPE, offset, limit, CollectionService.SOLR_SORT_FIELD, SORT_ORDER.asc); for (IndexableObject solrCollections : discoverResult.getIndexableObjects()) { @@ -864,9 +865,9 @@ public List findAdminAuthorizedCollection(Context context, String qu public long countAdminAuthorizedCollection(Context context, String query) throws SearchServiceException, SQLException { query = formatCustomQuery(query); - DiscoverResult discoverResult = getDiscoverResult(context, query + "search.resourcetype:" + + DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + IndexableCollection.TYPE, - null, null, null, null); + null, 0, null, null); return discoverResult.getTotalSearchResults(); } diff --git a/dspace-api/src/main/java/org/dspace/authorize/ResourcePolicyServiceImpl.java b/dspace-api/src/main/java/org/dspace/authorize/ResourcePolicyServiceImpl.java index 7b93b912378e..08a8a1463c03 100644 --- a/dspace-api/src/main/java/org/dspace/authorize/ResourcePolicyServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/authorize/ResourcePolicyServiceImpl.java @@ -19,6 +19,7 @@ import org.apache.commons.lang3.ObjectUtils; import org.apache.logging.log4j.Logger; import org.dspace.authorize.dao.ResourcePolicyDAO; +import org.dspace.authorize.service.AuthorizeService; import org.dspace.authorize.service.ResourcePolicyService; import org.dspace.content.DSpaceObject; import org.dspace.content.factory.ContentServiceFactory; @@ -51,6 +52,9 @@ public class ResourcePolicyServiceImpl implements ResourcePolicyService { @Autowired private GroupService groupService; + @Autowired + private AuthorizeService authorizeService; + protected ResourcePolicyServiceImpl() { } @@ -417,11 +421,11 @@ public boolean isMyResourcePolicy(Context context, EPerson eperson, Integer id) ResourcePolicy resourcePolicy = resourcePolicyDAO.findOneById(context, id); Group group = resourcePolicy.getGroup(); - if (resourcePolicy.getEPerson() != null && resourcePolicy.getEPerson().getID() == eperson.getID()) { + if (resourcePolicy.getEPerson() != null && resourcePolicy.getEPerson().getID().equals(eperson.getID())) { isMy = true; } else if (group != null && groupService.isMember(context, eperson, group)) { isMy = true; } - return isMy; + return isMy || authorizeService.isAdmin(context, eperson, resourcePolicy.getdSpaceObject()); } } diff --git a/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java b/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java index 351c36248209..be7a34086a46 100644 --- a/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java +++ b/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java @@ -422,9 +422,6 @@ private BrowseInfo browseByValue(BrowserScope bs) } } - // 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 @@ private BrowseInfo browseByValue(BrowserScope bs) 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 results = null; @@ -680,33 +680,9 @@ private int getTotalResults(boolean distinct) // 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; diff --git a/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java b/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java index f99aab852bf5..a0a7725fa13a 100644 --- a/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java +++ b/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java @@ -7,12 +7,17 @@ */ package org.dspace.browse; +import static org.dspace.discovery.SearchUtils.RESOURCE_ID_FIELD; +import static org.dspace.discovery.SearchUtils.RESOURCE_TYPE_FIELD; + import java.io.Serializable; import java.util.ArrayList; 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; @@ -21,7 +26,6 @@ import org.dspace.content.DSpaceObject; import org.dspace.content.Item; import org.dspace.core.Context; -import org.dspace.discovery.DiscoverFacetField; import org.dspace.discovery.DiscoverQuery; import org.dspace.discovery.DiscoverQuery.SORT_ORDER; import org.dspace.discovery.DiscoverResult; @@ -32,7 +36,6 @@ import org.dspace.discovery.SearchServiceException; import org.dspace.discovery.SearchUtils; import org.dspace.discovery.configuration.DiscoveryConfiguration; -import org.dspace.discovery.configuration.DiscoveryConfigurationParameters; import org.dspace.discovery.indexobject.IndexableItem; import org.dspace.services.factory.DSpaceServicesFactory; @@ -179,19 +182,30 @@ private DiscoverResult getSolrResponse() throws BrowseException { addLocationScopeFilter(query); addDefaultFilterQueries(query); if (distinct) { - DiscoverFacetField dff; - if (StringUtils.isNotBlank(startsWith)) { - dff = new DiscoverFacetField(facetField, - DiscoveryConfigurationParameters.TYPE_TEXT, -1, - DiscoveryConfigurationParameters.SORT.VALUE, startsWith); + // We use a json.facet query for metadata browsing because it allows us to limit the results + // while obtaining the total number of facet values with numBuckets:true and sort in reverse order + // Example of json.facet query: + // {"": {"type":"terms","field": "_filter", "limit":0, "offset":0, + // "sort":"index desc", "numBuckets":true, "prefix":""}} + ObjectNode jsonFacet = JsonNodeFactory.instance.objectNode(); + ObjectNode entriesFacet = JsonNodeFactory.instance.objectNode(); + entriesFacet.put("type", "terms"); + entriesFacet.put("field", facetField + "_filter"); + entriesFacet.put("limit", limit); + entriesFacet.put("offset", offset); + entriesFacet.put("numBuckets", true); + if (ascending) { + entriesFacet.put("sort", "index"); } else { - dff = new DiscoverFacetField(facetField, - DiscoveryConfigurationParameters.TYPE_TEXT, -1, - DiscoveryConfigurationParameters.SORT.VALUE); + entriesFacet.put("sort", "index desc"); + } + if (StringUtils.isNotBlank(startsWith)) { + // Add the prefix to the json facet query + entriesFacet.put("prefix", startsWith); } - query.addFacetField(dff); - query.setFacetMinCount(1); + jsonFacet.set(facetField, entriesFacet); query.setMaxResults(0); + query.addProperty("json.facet", jsonFacet.toString()); } else { query.setMaxResults(limit/* > 0 ? limit : 20*/); if (offset > 0) { @@ -248,8 +262,7 @@ public int doCountQuery() throws BrowseException { DiscoverResult resp = getSolrResponse(); int count = 0; if (distinct) { - List 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,26 +279,15 @@ public List doValueQuery() throws BrowseException { DiscoverResult resp = getSolrResponse(); List facet = resp.getFacetResult(facetField); int count = doCountQuery(); - int start = offset > 0 ? offset : 0; - int max = limit > 0 ? limit : count; //if negative, return everything + int max = facet.size(); List result = new ArrayList<>(); - if (ascending) { - for (int i = start; i < (start + max) && i < count; i++) { - FacetResult c = facet.get(i); - String freq = showFrequencies ? String.valueOf(c.getCount()) - : ""; - result.add(new String[] {c.getDisplayedValue(), - c.getAuthorityKey(), freq}); - } - } else { - for (int i = count - start - 1; i >= count - (start + max) - && i >= 0; i--) { - FacetResult c = facet.get(i); - String freq = showFrequencies ? String.valueOf(c.getCount()) - : ""; - result.add(new String[] {c.getDisplayedValue(), - c.getAuthorityKey(), freq}); - } + + for (int i = 0; i < max && i < count; i++) { + FacetResult c = facet.get(i); + String freq = showFrequencies ? String.valueOf(c.getCount()) + : ""; + result.add(new String[] {c.getDisplayedValue(), + c.getAuthorityKey(), freq}); } return result; @@ -309,8 +311,10 @@ public List doQuery() throws BrowseException { public String doMaxQuery(String column, String table, int itemID) throws BrowseException { DiscoverQuery query = new DiscoverQuery(); - query.setQuery("search.resourceid:" + itemID - + " AND search.resourcetype:" + IndexableItem.TYPE); + query.setQuery("*:*"); + query.addFilterQueries( + RESOURCE_ID_FIELD + ":" + itemID, + RESOURCE_TYPE_FIELD + ":" + IndexableItem.TYPE); query.setMaxResults(1); DiscoverResult resp = null; try { diff --git a/dspace-api/src/main/java/org/dspace/content/Collection.java b/dspace-api/src/main/java/org/dspace/content/Collection.java index 22293dd35ffc..33692d04b3d1 100644 --- a/dspace-api/src/main/java/org/dspace/content/Collection.java +++ b/dspace-api/src/main/java/org/dspace/content/Collection.java @@ -229,7 +229,7 @@ public String getLicenseCollection() { * @throws SQLException if database error */ public void setLicense(Context context, String license) throws SQLException { - getCollectionService().setMetadataSingleValue(context, this, MD_LICENSE, Item.ANY, license); + getCollectionService().setMetadataSingleValue(context, this, MD_LICENSE, null, license); } /** diff --git a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java index b800ce21a962..f5ef4f4b14a4 100644 --- a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java @@ -1021,7 +1021,8 @@ private DiscoverResult retrieveCollectionsWithSubmit(Context context, DiscoverQu if (StringUtils.isNotBlank(q)) { StringBuilder buildQuery = new StringBuilder(); String escapedQuery = ClientUtils.escapeQueryChars(q); - buildQuery.append("(").append(escapedQuery).append(" OR ").append(escapedQuery).append("*").append(")"); + buildQuery.append("(").append(escapedQuery).append(" OR dc.title_sort:*") + .append(escapedQuery).append("*").append(")"); discoverQuery.setQuery(buildQuery.toString()); } DiscoverResult resp = searchService.search(context, discoverQuery); diff --git a/dspace-api/src/main/java/org/dspace/content/DSpaceObjectServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/DSpaceObjectServiceImpl.java index c104833fe362..790a1424478c 100644 --- a/dspace-api/src/main/java/org/dspace/content/DSpaceObjectServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/DSpaceObjectServiceImpl.java @@ -187,11 +187,11 @@ public List getMetadata(T dso, String schema, String element, Str String authority) { List metadata = getMetadata(dso, schema, element, qualifier, lang); List result = new ArrayList<>(metadata); - if (!authority.equals(Item.ANY)) { + if (!Item.ANY.equals(authority)) { Iterator iterator = result.iterator(); while (iterator.hasNext()) { MetadataValue metadataValue = iterator.next(); - if (!authority.equals(metadataValue.getAuthority())) { + if (!StringUtils.equals(authority, metadataValue.getAuthority())) { iterator.remove(); } } @@ -509,7 +509,7 @@ protected boolean match(String schema, String element, String qualifier, MetadataField metadataField = metadataValue.getMetadataField(); MetadataSchema metadataSchema = metadataField.getMetadataSchema(); // We will attempt to disprove a match - if we can't we have a match - if (!element.equals(Item.ANY) && !element.equals(metadataField.getElement())) { + if (!Item.ANY.equals(element) && !StringUtils.equals(element, metadataField.getElement())) { // Elements do not match, no wildcard return false; } @@ -520,9 +520,9 @@ protected boolean match(String schema, String element, String qualifier, // Value is qualified, so no match return false; } - } else if (!qualifier.equals(Item.ANY)) { + } else if (!Item.ANY.equals(qualifier)) { // Not a wildcard, so qualifier must match exactly - if (!qualifier.equals(metadataField.getQualifier())) { + if (!StringUtils.equals(qualifier, metadataField.getQualifier())) { return false; } } @@ -533,15 +533,15 @@ protected boolean match(String schema, String element, String qualifier, // Value is qualified, so no match return false; } - } else if (!language.equals(Item.ANY)) { + } else if (!Item.ANY.equals(language)) { // Not a wildcard, so language must match exactly - if (!language.equals(metadataValue.getLanguage())) { + if (!StringUtils.equals(language, metadataValue.getLanguage())) { return false; } } - if (!schema.equals(Item.ANY)) { - if (metadataSchema != null && !metadataSchema.getName().equals(schema)) { + if (!Item.ANY.equals(schema)) { + if (!StringUtils.equals(schema, metadataSchema.getName())) { // The namespace doesn't match return false; } diff --git a/dspace-api/src/main/java/org/dspace/content/EntityServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/EntityServiceImpl.java index 9b28203827e0..e83178667840 100644 --- a/dspace-api/src/main/java/org/dspace/content/EntityServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/EntityServiceImpl.java @@ -60,7 +60,7 @@ public List getLeftRelations(Context context, Entity entity) { List fullList = entity.getRelationships(); List listToReturn = new LinkedList<>(); for (Relationship relationship : fullList) { - if (relationship.getLeftItem().getID() == entity.getItem().getID()) { + if (relationship.getLeftItem().getID().equals(entity.getItem().getID())) { listToReturn.add(relationship); } } @@ -72,7 +72,7 @@ public List getRightRelations(Context context, Entity entity) { List fullList = entity.getRelationships(); List listToReturn = new LinkedList<>(); for (Relationship relationship : fullList) { - if (relationship.getRightItem().getID() == entity.getItem().getID()) { + if (relationship.getRightItem().getID().equals(entity.getItem().getID())) { listToReturn.add(relationship); } } diff --git a/dspace-api/src/main/java/org/dspace/content/EntityTypeServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/EntityTypeServiceImpl.java index b5b066d9c36f..7df892cd56f5 100644 --- a/dspace-api/src/main/java/org/dspace/content/EntityTypeServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/EntityTypeServiceImpl.java @@ -30,6 +30,7 @@ import org.dspace.core.Context; import org.dspace.discovery.SolrSearchCore; import org.dspace.discovery.indexobject.IndexableCollection; +import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.eperson.service.GroupService; import org.springframework.beans.factory.annotation.Autowired; @@ -124,24 +125,33 @@ public void delete(Context context,EntityType entityType) throws SQLException, A public List getSubmitAuthorizedTypes(Context context) throws SQLException, SolrServerException, IOException { List types = new ArrayList<>(); - StringBuilder query = new StringBuilder(); - org.dspace.eperson.EPerson currentUser = context.getCurrentUser(); + StringBuilder query = null; + EPerson currentUser = context.getCurrentUser(); if (!authorizeService.isAdmin(context)) { String userId = ""; if (currentUser != null) { userId = currentUser.getID().toString(); + query = new StringBuilder(); + query.append("submit:(e").append(userId); } - query.append("submit:(e").append(userId); + Set groups = groupService.allMemberGroupsSet(context, currentUser); for (Group group : groups) { - query.append(" OR g").append(group.getID()); + if (query == null) { + query = new StringBuilder(); + query.append("submit:(g"); + } else { + query.append(" OR g"); + } + query.append(group.getID()); } query.append(")"); - } else { - query.append("*:*"); } - SolrQuery sQuery = new SolrQuery(query.toString()); + SolrQuery sQuery = new SolrQuery("*:*"); + if (query != null) { + sQuery.addFilterQuery(query.toString()); + } sQuery.addFilterQuery("search.resourcetype:" + IndexableCollection.TYPE); sQuery.setRows(0); sQuery.addFacetField("search.entitytype"); diff --git a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java index 70bdf4b7d950..157b891486f0 100644 --- a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java @@ -67,6 +67,7 @@ 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.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 implements It @Autowired private QAEventsDAO qaEventsDao; + @Autowired + private VersionHistoryService versionHistoryService; + protected ItemServiceImpl() { } @@ -851,6 +858,7 @@ protected void rawDelete(Context context, Item item) throws AuthorizeException, DOI doi = doiService.findDOIByDSpaceObject(context, item); if (doi != null) { doi.setDSpaceObject(null); + doi.setStatus(DOIIdentifierProvider.TO_BE_DELETED); } // remove version attached to the item @@ -1799,7 +1807,7 @@ protected void moveSingleMetadataValue(Context context, Item dso, int place, Met //Retrieve the applicable relationship Relationship rs = relationshipService.find(context, ((RelationshipMetadataValue) rr).getRelationshipId()); - if (rs.getLeftItem() == dso) { + if (rs.getLeftItem().equals(dso)) { rs.setLeftPlace(place); } else { rs.setRightPlace(place); @@ -1931,4 +1939,40 @@ private void deleteOrcidQueueRecords(Context context, Item item) throws SQLExcep } } + @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()); + + } + } diff --git a/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java index 8bc34d3f5ed1..757c5d0cd529 100644 --- a/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java @@ -23,6 +23,7 @@ import org.dspace.core.Context; import org.dspace.handle.factory.HandleServiceFactory; import org.dspace.scripts.handler.DSpaceRunnableHandler; +import org.dspace.services.ConfigurationService; import org.springframework.beans.factory.annotation.Autowired; /** @@ -36,6 +37,11 @@ public class MetadataDSpaceCsvExportServiceImpl implements MetadataDSpaceCsvExpo @Autowired private DSpaceObjectUtils dSpaceObjectUtils; + @Autowired + private ConfigurationService configurationService; + + private int csxExportLimit = -1; + @Override public DSpaceCSV handleExport(Context context, boolean exportAllItems, boolean exportAllMetadata, String identifier, DSpaceRunnableHandler handler) throws Exception { @@ -43,7 +49,7 @@ public DSpaceCSV handleExport(Context context, boolean exportAllItems, boolean e if (exportAllItems) { handler.logInfo("Exporting whole repository WARNING: May take some time!"); - toExport = itemService.findAll(context); + toExport = itemService.findAll(context, getCsvExportLimit(), 0); } else { DSpaceObject dso = HandleServiceFactory.getInstance().getHandleService() .resolveToObject(context, identifier); @@ -63,7 +69,7 @@ public DSpaceCSV handleExport(Context context, boolean exportAllItems, boolean e } else if (dso.getType() == Constants.COLLECTION) { handler.logInfo("Exporting collection '" + dso.getName() + "' (" + identifier + ")"); Collection collection = (Collection) dso; - toExport = itemService.findByCollection(context, collection); + toExport = itemService.findByCollection(context, collection, getCsvExportLimit(), 0); } else if (dso.getType() == Constants.COMMUNITY) { handler.logInfo("Exporting community '" + dso.getName() + "' (" + identifier + ")"); toExport = buildFromCommunity(context, (Community) dso); @@ -74,18 +80,21 @@ public DSpaceCSV handleExport(Context context, boolean exportAllItems, boolean e } } - DSpaceCSV csv = this.export(context, toExport, exportAllMetadata); + DSpaceCSV csv = this.export(context, toExport, exportAllMetadata, handler); return csv; } @Override - public DSpaceCSV export(Context context, Iterator toExport, boolean exportAll) throws Exception { + public DSpaceCSV export(Context context, Iterator toExport, + boolean exportAll, DSpaceRunnableHandler handler) throws Exception { Context.Mode originalMode = context.getCurrentMode(); context.setMode(Context.Mode.READ_ONLY); - // Process each item + // Process each item until we reach the limit + int itemExportLimit = getCsvExportLimit(); DSpaceCSV csv = new DSpaceCSV(exportAll); - while (toExport.hasNext()) { + + for (int itemsAdded = 0; toExport.hasNext() && itemsAdded < itemExportLimit; itemsAdded++) { Item item = toExport.next(); csv.addItem(item); context.uncacheEntity(item); @@ -97,8 +106,9 @@ public DSpaceCSV export(Context context, Iterator toExport, boolean export } @Override - public DSpaceCSV export(Context context, Community community, boolean exportAll) throws Exception { - return export(context, buildFromCommunity(context, community), exportAll); + public DSpaceCSV export(Context context, Community community, + boolean exportAll, DSpaceRunnableHandler handler) throws Exception { + return export(context, buildFromCommunity(context, community), exportAll, handler); } /** @@ -117,21 +127,30 @@ private Iterator buildFromCommunity(Context context, Community community) // Add all the collections List collections = community.getCollections(); for (Collection collection : collections) { - Iterator items = itemService.findByCollection(context, collection); - while (items.hasNext()) { + // Never obtain more items than the configured limit + Iterator items = itemService.findByCollection(context, collection, getCsvExportLimit(), 0); + while (result.size() < getCsvExportLimit() && items.hasNext()) { result.add(items.next()); } } - // Add all the sub-communities + // Add all the sub-communities List communities = community.getSubcommunities(); for (Community subCommunity : communities) { Iterator items = buildFromCommunity(context, subCommunity); - while (items.hasNext()) { + while (result.size() < getCsvExportLimit() && items.hasNext()) { result.add(items.next()); } } return result.iterator(); } + + @Override + public int getCsvExportLimit() { + if (csxExportLimit == -1) { + csxExportLimit = configurationService.getIntProperty("bulkedit.export.max.items", 500); + } + return csxExportLimit; + } } diff --git a/dspace-api/src/main/java/org/dspace/content/MetadataValue.java b/dspace-api/src/main/java/org/dspace/content/MetadataValue.java index 279bdd67c243..dc45579f4ef9 100644 --- a/dspace-api/src/main/java/org/dspace/content/MetadataValue.java +++ b/dspace-api/src/main/java/org/dspace/content/MetadataValue.java @@ -19,6 +19,7 @@ import jakarta.persistence.SequenceGenerator; import jakarta.persistence.Table; import jakarta.persistence.Transient; +import org.apache.commons.lang3.StringUtils; import org.dspace.core.Context; import org.dspace.core.HibernateProxyHelper; import org.dspace.core.ReloadableEntity; @@ -139,6 +140,9 @@ public String getLanguage() { * @param language new language */ public void setLanguage(String language) { + if (StringUtils.equals(language, Item.ANY)) { + language = null; + } this.language = language; } diff --git a/dspace-api/src/main/java/org/dspace/content/WorkspaceItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/WorkspaceItemServiceImpl.java index 1da9e6e44a6a..543f5a55efe2 100644 --- a/dspace-api/src/main/java/org/dspace/content/WorkspaceItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/WorkspaceItemServiceImpl.java @@ -178,6 +178,14 @@ public WorkspaceItem create(Context context, Collection collection, UUID uuid, b @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()); diff --git a/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java index f4d1f02710e1..bbe8e4461fe0 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java @@ -577,7 +577,7 @@ public DSpaceControlledVocabularyIndex getVocabularyIndex(String nameVocab) { .collect(Collectors.toList())); } DiscoverySearchFilterFacet matchingFacet = null; - for (DiscoverySearchFilterFacet facetConfig : searchConfigurationService.getAllFacetsConfig()) { + for (DiscoverySearchFilterFacet facetConfig : searchConfigurationService.getAllUniqueFacetsConfig()) { boolean coversAllFieldsFromVocab = true; for (String fieldFromVocab: metadataFields) { boolean coversFieldFromVocab = false; diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/METSDisseminationCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/METSDisseminationCrosswalk.java index b8a4a8aef390..5ceacc933e4c 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/METSDisseminationCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/METSDisseminationCrosswalk.java @@ -14,6 +14,7 @@ import java.util.List; import org.apache.commons.lang3.ArrayUtils; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.packager.PackageDisseminator; @@ -129,7 +130,7 @@ public Element disseminateElement(Context context, DSpaceObject dso) try { //Return just the root Element of the METS file - SAXBuilder builder = new SAXBuilder(); + SAXBuilder builder = XMLUtils.getSAXBuilder(); Document metsDocument = builder.build(tempFile); return metsDocument.getRootElement(); } catch (JDOMException je) { diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/MODSDisseminationCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/MODSDisseminationCrosswalk.java index 1e63be5ba1b9..205b3ef5b343 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/MODSDisseminationCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/MODSDisseminationCrosswalk.java @@ -22,6 +22,7 @@ import org.apache.commons.lang3.ArrayUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; import org.dspace.content.Community; @@ -144,7 +145,7 @@ public static String[] getPluginNames() { MODS_NS.getURI() + " " + MODS_XSD; private static final XMLOutputter outputUgly = new XMLOutputter(); - private static final SAXBuilder builder = new SAXBuilder(); + private static final SAXBuilder builder = XMLUtils.getSAXBuilder(); private Map modsMap = null; diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/QDCCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/QDCCrosswalk.java index 2fdbaaad003e..51e6357d93e1 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/QDCCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/QDCCrosswalk.java @@ -22,6 +22,7 @@ import org.apache.commons.lang3.ArrayUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; @@ -125,7 +126,7 @@ public class QDCCrosswalk extends SelfNamedPlugin // XML schemaLocation fragment for this crosswalk, from config. private String schemaLocation = null; - private static final SAXBuilder builder = new SAXBuilder(); + private static final SAXBuilder builder = XMLUtils.getSAXBuilder(); protected ItemService itemService = ContentServiceFactory.getInstance().getItemService(); diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/RoleCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/RoleCrosswalk.java index 2c763036ce33..8d5bf49902cc 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/RoleCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/RoleCrosswalk.java @@ -13,6 +13,7 @@ import java.sql.SQLException; import java.util.List; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.packager.PackageDisseminator; @@ -208,7 +209,7 @@ public Element disseminateElement(Context context, DSpaceObject dso) try { //Try to parse our XML results (which were disseminated by the Packager) - SAXBuilder builder = new SAXBuilder(); + SAXBuilder builder = XMLUtils.getSAXBuilder(); Document xmlDocument = builder.build(tempFile); //If XML parsed successfully, return root element of doc if (xmlDocument != null && xmlDocument.hasRootElement()) { diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTIngestionCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTIngestionCrosswalk.java index 63ef5f7336c7..b07b2b2228e4 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTIngestionCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTIngestionCrosswalk.java @@ -18,6 +18,7 @@ import org.apache.commons.lang3.ArrayUtils; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; import org.dspace.content.Community; @@ -297,7 +298,7 @@ public static void main(String[] argv) throws Exception { "Failed to initialize transformer, probably error loading stylesheet."); } - SAXBuilder builder = new SAXBuilder(); + SAXBuilder builder = XMLUtils.getSAXBuilder(); Document inDoc = builder.build(new FileInputStream(argv[i + 1])); XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat()); List dimList; diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/ItemDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/ItemDAOImpl.java index 3be39f1788fb..bd042648384b 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/ItemDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/ItemDAOImpl.java @@ -441,7 +441,7 @@ public int countItems(Context context, List collections, boolean inc public Iterator findByLastModifiedSince(Context context, Date since) throws SQLException { Query query = createQuery(context, - "SELECT i.id FROM Item i WHERE last_modified > :last_modified ORDER BY id"); + "SELECT i.id FROM Item i WHERE lastModified > :last_modified ORDER BY id"); query.setParameter("last_modified", since, TemporalType.TIMESTAMP); @SuppressWarnings("unchecked") List uuids = query.getResultList(); diff --git a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java index 0ed0abe21825..77236be9d525 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java @@ -11,8 +11,6 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; -import java.net.URL; -import java.net.URLConnection; import java.sql.SQLException; import java.util.Iterator; import java.util.List; @@ -21,7 +19,11 @@ import java.util.zip.ZipFile; import org.apache.commons.collections4.CollectionUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Bitstream; import org.dspace.content.BitstreamFormat; @@ -1310,13 +1312,12 @@ protected static InputStream getFileInputStream(File pkgFile, if (params.getBooleanProperty("manifestOnly", false)) { // NOTE: since we are only dealing with a METS manifest, // we will assume all external files are available via URLs. - try { + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { // attempt to open a connection to given URL - URL fileURL = new URL(path); - URLConnection connection = fileURL.openConnection(); - - // open stream to access file contents - return connection.getInputStream(); + try (CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(path))) { + // open stream to access file contents + return httpResponse.getEntity().getContent(); + } } catch (IOException io) { log .error("Unable to retrieve external file from URL '" diff --git a/dspace-api/src/main/java/org/dspace/content/packager/METSManifest.java b/dspace-api/src/main/java/org/dspace/content/packager/METSManifest.java index 3399bdf0f07e..a1ed3c124374 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/METSManifest.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/METSManifest.java @@ -20,6 +20,7 @@ import org.apache.commons.codec.binary.Base64; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Bitstream; import org.dspace.content.Bundle; @@ -265,12 +266,13 @@ protected METSManifest(SAXBuilder builder, Element mets, String configName) { public static METSManifest create(InputStream is, boolean validate, String configName) throws IOException, MetadataValidationException { - SAXBuilder builder = new SAXBuilder(validate); + SAXBuilder builder = XMLUtils.getSAXBuilder(); builder.setIgnoringElementContentWhitespace(true); // Set validation feature if (validate) { + builder.setValidation(true); builder.setFeature("http://apache.org/xml/features/validation/schema", true); // Tell the parser where local copies of schemas are, to speed up @@ -278,10 +280,6 @@ public static METSManifest create(InputStream is, boolean validate, String confi if (localSchemas.length() > 0) { builder.setProperty("http://apache.org/xml/properties/schema/external-schemaLocation", localSchemas); } - } else { - // disallow DTD parsing to ensure no XXE attacks can occur. - // See https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html - builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); } // Parse the METS file diff --git a/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java b/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java index ca27abe20614..a0ac43282c32 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java @@ -21,6 +21,7 @@ import org.apache.commons.codec.DecoderException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; import org.dspace.content.Community; @@ -385,7 +386,7 @@ public void ingestStream(Context context, DSpaceObject parent, Document document; try { - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilderFactory dbf = XMLUtils.getDocumentBuilderFactory(); dbf.setIgnoringComments(true); dbf.setCoalescing(true); DocumentBuilder db = dbf.newDocumentBuilder(); @@ -419,7 +420,7 @@ public DSpaceObject ingest(Context context, DSpaceObject parent, Document document; try { - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilderFactory dbf = XMLUtils.getDocumentBuilderFactory(); dbf.setIgnoringComments(true); dbf.setCoalescing(true); DocumentBuilder db = dbf.newDocumentBuilder(); diff --git a/dspace-api/src/main/java/org/dspace/content/service/DSpaceObjectService.java b/dspace-api/src/main/java/org/dspace/content/service/DSpaceObjectService.java index 606f5bb7c02f..c008985a2834 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/DSpaceObjectService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/DSpaceObjectService.java @@ -202,7 +202,7 @@ public List getMetadata(T dSpaceObject, String schema, String ele * Get the value(s) of a metadata field. * @param dSpaceObject the object whose metadata are sought. * @param mdString the name of the field: {@code schema.element.qualifier}. - * @param authority name of the authority which controls these values, or null. + * @param authority name of the authority which controls these values, or Item.ANY, or null. * @return all matching metadata values, or null if none. */ public List getMetadata(T dSpaceObject, String mdString, String authority); @@ -216,7 +216,7 @@ public List getMetadata(T dSpaceObject, String schema, String ele * @param lang the language of the requested field value(s), * null if explicitly no language, * or {@link org.dspace.content.Item.ANY} to match all languages. - * @param authority name of the authority which controls these values, or null. + * @param authority name of the authority which controls these values, or Item.ANY, or null. * @return value(s) of the indicated field for the given DSO, or null. */ public List getMetadata(T dSpaceObject, String schema, diff --git a/dspace-api/src/main/java/org/dspace/content/service/ItemService.java b/dspace-api/src/main/java/org/dspace/content/service/ItemService.java index 47d2d5bdaa88..3fea75665bcb 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/ItemService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/ItemService.java @@ -1009,4 +1009,14 @@ List getMetadata(Item item, String schema, String element, String */ 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; } diff --git a/dspace-api/src/main/java/org/dspace/content/service/MetadataDSpaceCsvExportService.java b/dspace-api/src/main/java/org/dspace/content/service/MetadataDSpaceCsvExportService.java index d3fc2e823669..a951e8cf7763 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/MetadataDSpaceCsvExportService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/MetadataDSpaceCsvExportService.java @@ -44,7 +44,8 @@ public DSpaceCSV handleExport(Context context, boolean exportAllItems, boolean e * @return A DSpaceCSV object containing the exported information * @throws Exception If something goes wrong */ - public DSpaceCSV export(Context context, Iterator toExport, boolean exportAll) throws Exception; + public DSpaceCSV export(Context context, Iterator toExport, + boolean exportAll, DSpaceRunnableHandler handler) throws Exception; /** * This method will export all the Items within the given Community to a DSpaceCSV @@ -54,6 +55,9 @@ public DSpaceCSV handleExport(Context context, boolean exportAllItems, boolean e * @return A DSpaceCSV object containing the exported information * @throws Exception If something goes wrong */ - public DSpaceCSV export(Context context, Community community, boolean exportAll) throws Exception; + public DSpaceCSV export(Context context, Community community, + boolean exportAll, DSpaceRunnableHandler handler) throws Exception; -} \ No newline at end of file + int getCsvExportLimit(); + +} diff --git a/dspace-api/src/main/java/org/dspace/core/Context.java b/dspace-api/src/main/java/org/dspace/core/Context.java index 02a3fee09f8a..dab6ab7fbd66 100644 --- a/dspace-api/src/main/java/org/dspace/core/Context.java +++ b/dspace-api/src/main/java/org/dspace/core/Context.java @@ -883,7 +883,19 @@ public E reloadEntity(E entity) throws SQLException } /** - * 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 The class of the entity. The entity must implement the {@link ReloadableEntity} interface. diff --git a/dspace-api/src/main/java/org/dspace/core/DBConnection.java b/dspace-api/src/main/java/org/dspace/core/DBConnection.java index 66e4a65dbfe1..c9c4ce0953e4 100644 --- a/dspace-api/src/main/java/org/dspace/core/DBConnection.java +++ b/dspace-api/src/main/java/org/dspace/core/DBConnection.java @@ -124,28 +124,38 @@ public interface DBConnection { 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 type of {@link entity} - * @param entity The DSpace object to reload + * @param 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 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. * - *

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 + *

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 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. + * + *

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 Type of entity. + * @param entity The entity to decache. + * @throws SQLException passed through. */ public void uncacheEntity(E entity) throws SQLException; diff --git a/dspace-api/src/main/java/org/dspace/core/Email.java b/dspace-api/src/main/java/org/dspace/core/Email.java index bb434c07cb96..74a48b3d82c9 100644 --- a/dspace-api/src/main/java/org/dspace/core/Email.java +++ b/dspace-api/src/main/java/org/dspace/core/Email.java @@ -55,7 +55,7 @@ import org.dspace.services.factory.DSpaceServicesFactory; /** - * Class representing an e-mail message. The {@link send} method causes the + * Builder representing an e-mail message. The {@link send} method causes the * assembled message to be formatted and sent. *

* Typical use: @@ -168,6 +168,9 @@ public class Email { */ private String charset; + /** The message being assembled. */ + MimeMessage message; + private static final Logger LOG = LogManager.getLogger(); /** Velocity template settings. */ @@ -188,6 +191,9 @@ public class Email { /** Velocity template for a message body */ private Template template; + /** The message text. */ + private String body; + /** * Create a new email message. */ @@ -254,9 +260,15 @@ public void setReplyTo(String email) { /** * Fill out the next argument in the template. * - * @param arg the value for the next argument + * @param arg the value for the next argument. If {@code null}, + * a zero-length string is substituted. */ public void addArgument(Object arg) { + if (null == arg) { + arg = ""; + LOG.warn("Null argument {} to email template {} replaced with zero-length string", + arguments.size(), contentName); + } arguments.add(arg); } @@ -327,7 +339,27 @@ public void reset() { } /** - * Sends the email. If the template defines a Velocity context property + * Sends the email. If sending is disabled then the assembled message is + * logged instead. + * + * @throws MessagingException if there was a problem sending the mail. + * @throws IOException if IO error + */ + public void send() throws MessagingException, IOException { + build(); + + ConfigurationService config + = DSpaceServicesFactory.getInstance().getConfigurationService(); + boolean disabled = config.getBooleanProperty("mail.server.disabled", false); + if (disabled) { + LOG.info(format(message, body)); + } else { + Transport.send(message); + } + } + + /** + * Build the message. If the template defines a Velocity context property * named among the values of DSpace configuration property * {@code mail.message.headers} then that name and its value will be added * to the message's headers. @@ -336,11 +368,12 @@ public void reset() { * called, the value of any "subject" property will be used as if setSubject * had been called with that value. Thus a template may define its subject, * but the caller may override it. - * - * @throws MessagingException if there was a problem sending the mail. - * @throws IOException if IO error + * + * @throws MessagingException if there is no template, or passed through. + * @throws IOException passed through. */ - public void send() throws MessagingException, IOException { + void build() + throws MessagingException, IOException { if (null == template) { // No template -- no content -- PANIC!!! throw new MessagingException("Email has no body"); @@ -351,7 +384,6 @@ public void send() throws MessagingException, IOException { // Get the mail configuration properties String from = config.getProperty("mail.from.address"); - boolean disabled = config.getBooleanProperty("mail.server.disabled", false); // If no character set specified, attempt to retrieve a default if (charset == null) { @@ -362,7 +394,7 @@ public void send() throws MessagingException, IOException { Session session = DSpaceServicesFactory.getInstance().getEmailService().getSession(); // Create message - MimeMessage message = new MimeMessage(session); + message = new MimeMessage(session); // Set the recipients of the message for (String recipient : recipients) { @@ -385,7 +417,7 @@ public void send() throws MessagingException, IOException { LOG.error("Template not merged: {}", ex.getMessage()); throw new MessagingException("Template not merged", ex); } - String fullMessage = writer.toString(); + body = writer.toString(); // Set some message header fields Date date = new Date(); @@ -412,20 +444,19 @@ public void send() throws MessagingException, IOException { message.setSubject(subject); } - // Add attachments - if (attachments.isEmpty() && moreAttachments.isEmpty()) { - // If a character set has been specified, or a default exists + // Attach the body. + if (attachments.isEmpty() && moreAttachments.isEmpty()) { // Flat body. if (charset != null) { - message.setText(fullMessage, charset); + message.setText(body, charset); } else { - message.setText(fullMessage); + message.setText(body); } - } else { + } else { // Add attachments. Multipart multipart = new MimeMultipart(); // create the first part of the email BodyPart messageBodyPart = new MimeBodyPart(); - messageBodyPart.setText(fullMessage); + messageBodyPart.setText(body); multipart.addBodyPart(messageBodyPart); // Add file attachments @@ -457,30 +488,47 @@ public void send() throws MessagingException, IOException { replyToAddr[0] = new InternetAddress(replyTo); message.setReplyTo(replyToAddr); } + } - if (disabled) { - StringBuilder text = new StringBuilder( - "Message not sent due to mail.server.disabled:\n"); - - Enumeration headers = message.getAllHeaderLines(); - while (headers.hasMoreElements()) { - text.append(headers.nextElement()).append('\n'); - } + /** + * Flatten the email into a string. + * + * @param message the message headers, attachments, etc. + * @param body the message body. + * @return stringified email message. + * @throws MessagingException passed through. + */ + private String format(MimeMessage message, String body) + throws MessagingException { + StringBuilder text = new StringBuilder( + "Message not sent due to mail.server.disabled:\n"); + + Enumeration headers = message.getAllHeaderLines(); + while (headers.hasMoreElements()) { + text.append(headers.nextElement()).append('\n'); + } - if (!attachments.isEmpty()) { - text.append("\nAttachments:\n"); - for (FileAttachment f : attachments) { - text.append(f.name).append('\n'); - } - text.append('\n'); + if (!attachments.isEmpty()) { + text.append("\nAttachments:\n"); + for (FileAttachment f : attachments) { + text.append(f.name).append('\n'); } + text.append('\n'); + } - text.append('\n').append(fullMessage); + text.append('\n').append(body); + return text.toString(); + } - LOG.info(text.toString()); - } else { - Transport.send(message); - } + /** + * Get the formatted message for testing. + * + * @return the message flattened to a String. + * @throws MessagingException passed through. + */ + String getMessage() + throws MessagingException { + return format(message, body); } /** diff --git a/dspace-api/src/main/java/org/dspace/core/HibernateDBConnection.java b/dspace-api/src/main/java/org/dspace/core/HibernateDBConnection.java index a867849077a3..806930d0364a 100644 --- a/dspace-api/src/main/java/org/dspace/core/HibernateDBConnection.java +++ b/dspace-api/src/main/java/org/dspace/core/HibernateDBConnection.java @@ -242,6 +242,11 @@ private void configureDatabaseMode() throws SQLException { } } + @Override + public void uncacheEntities() throws SQLException { + getSession().clear(); + } + /** * Evict an entity from the hibernate cache. *

diff --git a/dspace-api/src/main/java/org/dspace/core/Utils.java b/dspace-api/src/main/java/org/dspace/core/Utils.java index ea9ed57eca04..a1294c3317ce 100644 --- a/dspace-api/src/main/java/org/dspace/core/Utils.java +++ b/dspace-api/src/main/java/org/dspace/core/Utils.java @@ -506,4 +506,5 @@ public static String interpolateConfigsInString(String string) { ConfigurationService config = DSpaceServicesFactory.getInstance().getConfigurationService(); return StringSubstitutor.replace(string, config.getProperties()); } + } diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java b/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java index fbc6eebdb5b8..020331842703 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java @@ -9,16 +9,22 @@ import java.io.IOException; import java.net.HttpURLConnection; -import java.net.URL; import java.util.ArrayList; import java.util.List; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.content.DSpaceObject; 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 +48,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 +119,8 @@ protected List getURLs(Item item) { */ 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,15 +138,21 @@ protected boolean checkURL(String url, StringBuilder results) { * @param url The url to open * @return The HTTP response code (e.g. 200 / 301 / 404 / 500) */ - protected int getResponseStatus(String url) { - try { - URL theURL = new URL(url); - HttpURLConnection connection = (HttpURLConnection) theURL.openConnection(); - int code = connection.getResponseCode(); - connection.disconnect(); - - return code; - + protected int getResponseStatus(String url, int redirects) { + RequestConfig config = RequestConfig.custom().setRedirectsEnabled(true).build(); + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().buildWithRequestConfig(config)) { + CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(url)); + int statusCode = httpResponse.getStatusLine().getStatusCode(); + 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)) { + String newUrl = httpResponse.getFirstHeader("Location").getValue(); + if (newUrl != null && (maxRedirect >= redirects || maxRedirect == -1)) { + redirects++; + return getResponseStatus(newUrl, redirects); + } + } + return statusCode; } catch (IOException ioe) { // Must be a bad URL log.debug("Bad link: " + ioe.getMessage()); diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/ClamScan.java b/dspace-api/src/main/java/org/dspace/ctask/general/ClamScan.java index 47fa6ee6452d..5a717fe1e479 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/ClamScan.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/ClamScan.java @@ -15,13 +15,12 @@ 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 int perform(DSpaceObject dso) throws IOException { } try { - Bundle bundle = itemService.getBundles(item, "ORIGINAL").get(0); - results = new ArrayList<>(); + List 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(); for (Bitstream bitstream : bundle.getBitstreams()) { InputStream inputstream = bitstreamService.retrieve(Curator.curationContext(), bitstream); logDebugMessage("Scanning " + bitstream.getName() + " . . . "); @@ -121,10 +125,11 @@ public int perform(DSpaceObject dso) throws IOException { } } - } 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(); } diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/CreateMissingIdentifiers.java b/dspace-api/src/main/java/org/dspace/ctask/general/CreateMissingIdentifiers.java index 9639461426ef..0734d60946bc 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/CreateMissingIdentifiers.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/CreateMissingIdentifiers.java @@ -10,6 +10,7 @@ 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.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 int perform(DSpaceObject dso) 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 int perform(DSpaceObject dso) .getInstance() .getIdentifierService(); + // XXX Temporary escape when an incompatible provider is configured. + // XXX Remove this when the provider is fixed. + List 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); diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java b/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java index 5891fa017cb0..1b618ad31fb7 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java @@ -30,13 +30,14 @@ import javax.xml.xpath.XPathFactory; import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; @@ -176,7 +177,7 @@ public void init(Curator curator, String taskId) throws IOException { fieldSeparator = (fldSep != null) ? fldSep : " "; urlTemplate = taskProperty("template"); templateParam = urlTemplate.substring(urlTemplate.indexOf("{") + 1, - urlTemplate.indexOf("}")); + urlTemplate.indexOf("}")); String[] parsed = parseTransform(templateParam); lookupField = parsed[0]; lookupTransform = parsed[1]; @@ -204,13 +205,9 @@ public void init(Curator curator, String taskId) throws IOException { } } // initialize response document parser - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(true); try { - // disallow DTD parsing to ensure no XXE attacks can occur - // See https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html - factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - factory.setXIncludeAware(false); + DocumentBuilderFactory factory = XMLUtils.getDocumentBuilderFactory(); + factory.setNamespaceAware(true); docBuilder = factory.newDocumentBuilder(); } catch (ParserConfigurationException pcE) { log.error("caught exception: " + pcE); @@ -255,53 +252,50 @@ public int perform(DSpaceObject dso) throws IOException { } protected int callService(String value, Item item, StringBuilder resultSb) throws IOException { - String callUrl = urlTemplate.replaceAll("\\{" + templateParam + "\\}", value); - CloseableHttpClient client = HttpClientBuilder.create().build(); - HttpGet req = new HttpGet(callUrl); - for (Map.Entry entry : headers.entrySet()) { - req.addHeader(entry.getKey(), entry.getValue()); - } - HttpResponse resp = client.execute(req); - int status = Curator.CURATE_ERROR; - int statusCode = resp.getStatusLine().getStatusCode(); - if (statusCode == HttpStatus.SC_OK) { - HttpEntity entity = resp.getEntity(); - if (entity != null) { - // boiler-plate handling taken from Apache 4.1 javadoc - InputStream instream = entity.getContent(); - try { - // This next line triggers a false-positive XXE warning from LGTM, even though we disallow DTD - // parsing during initialization of docBuilder in init() - Document doc = docBuilder.parse(instream); // lgtm [java/xxe] - status = processResponse(doc, item, resultSb); - } catch (SAXException saxE) { - log.error("caught exception: " + saxE); - resultSb.append(" unable to read response document"); - } catch (RuntimeException ex) { - // In case of an unexpected exception you may want to abort - // the HTTP request in order to shut down the underlying - // connection and release it back to the connection manager. - req.abort(); - log.error("caught exception: " + ex); - throw ex; - } finally { - // Closing the input stream will trigger connection release - instream.close(); + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { + HttpGet req = new HttpGet(callUrl); + for (Map.Entry entry : headers.entrySet()) { + req.addHeader(entry.getKey(), entry.getValue()); + } + try (CloseableHttpResponse resp = client.execute(req)) { + int status = Curator.CURATE_ERROR; + int statusCode = resp.getStatusLine().getStatusCode(); + if (statusCode == HttpStatus.SC_OK) { + HttpEntity entity = resp.getEntity(); + if (entity != null) { + // boiler-plate handling taken from Apache 4.1 javadoc + InputStream instream = entity.getContent(); + try { + // This next line triggers a false-positive XXE warning from LGTM, even though + // we disallow DTD parsing during initialization of docBuilder in init() + Document doc = docBuilder.parse(instream); // lgtm [java/xxe] + status = processResponse(doc, item, resultSb); + } catch (SAXException saxE) { + log.error("caught exception: " + saxE); + resultSb.append(" unable to read response document"); + } catch (RuntimeException ex) { + // In case of an unexpected exception you may want to abort + // the HTTP request in order to shut down the underlying + // connection and release it back to the connection manager. + req.abort(); + log.error("caught exception: " + ex); + throw ex; + } finally { + // Closing the input stream will trigger connection release + instream.close(); + } + } else { + log.error(" obtained no valid service response"); + resultSb.append("no service response"); + } + } else { + log.error("service returned non-OK status: " + statusCode); + resultSb.append("no service response"); } - // When HttpClient instance is no longer needed, - // shut down the connection manager to ensure - // immediate deallocation of all system resources - client.close(); - } else { - log.error(" obtained no valid service response"); - resultSb.append("no service response"); + return status; } - } else { - log.error("service returned non-OK status: " + statusCode); - resultSb.append("no service response"); } - return status; } protected int processResponse(Document doc, Item item, StringBuilder resultSb) throws IOException { diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/MicrosoftTranslator.java b/dspace-api/src/main/java/org/dspace/ctask/general/MicrosoftTranslator.java index 49c0c36a5917..af9c2de0c8b1 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/MicrosoftTranslator.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/MicrosoftTranslator.java @@ -12,12 +12,12 @@ import java.nio.charset.StandardCharsets; import org.apache.commons.io.IOUtils; -import org.apache.http.HttpResponse; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; @@ -60,22 +60,20 @@ protected String translateText(String from, String to, String text) throws IOExc String url = baseUrl + "?appId=" + apiKey; url += "&to=" + to + "&from=" + from + "&text=" + text; - try (CloseableHttpClient client = HttpClientBuilder.create().build()) { + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { HttpGet hm = new HttpGet(url); - HttpResponse httpResponse = client.execute(hm); - log.debug("Response code from API call is " + httpResponse); - - if (httpResponse.getStatusLine().getStatusCode() == 200) { - String response = IOUtils.toString(httpResponse.getEntity().getContent(), - StandardCharsets.ISO_8859_1); - response = response - .replaceAll("", ""); - response = response.replaceAll("", ""); - translatedText = response; + try (CloseableHttpResponse httpResponse = client.execute(hm)) { + log.debug("Response code from API call is " + httpResponse); + if (httpResponse.getStatusLine().getStatusCode() == 200) { + String response = IOUtils.toString(httpResponse.getEntity().getContent(), + StandardCharsets.ISO_8859_1); + response = response + .replaceAll("", ""); + response = response.replaceAll("", ""); + translatedText = response; + } } } - return translatedText; } -} - +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/curate/Curation.java b/dspace-api/src/main/java/org/dspace/curate/Curation.java index 4d70286e79e0..b894dcd85f03 100644 --- a/dspace-api/src/main/java/org/dspace/curate/Curation.java +++ b/dspace-api/src/main/java/org/dspace/curate/Curation.java @@ -24,6 +24,8 @@ import org.apache.commons.cli.ParseException; import org.apache.commons.io.output.NullOutputStream; +import org.dspace.app.util.DSpaceObjectUtilsImpl; +import org.dspace.app.util.service.DSpaceObjectUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.factory.ContentServiceFactory; @@ -35,6 +37,7 @@ import org.dspace.handle.factory.HandleServiceFactory; import org.dspace.handle.service.HandleService; import org.dspace.scripts.DSpaceRunnable; +import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.utils.DSpace; /** @@ -45,7 +48,9 @@ public class Curation extends DSpaceRunnable { protected EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); - + protected DSpaceObjectUtils dspaceObjectUtils = DSpaceServicesFactory.getInstance().getServiceManager() + .getServiceByName(DSpaceObjectUtilsImpl.class.getName(), DSpaceObjectUtilsImpl.class); + HandleService handleService = HandleServiceFactory.getInstance().getHandleService(); protected Context context; private CurationClientOptions curationClientOptions; @@ -165,7 +170,7 @@ private long runQueue(TaskQueue queue, Curator curator) throws SQLException, Aut * 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 +190,7 @@ private Curator initCurator() throws FileNotFoundException { 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 +305,17 @@ private void initGeneralLineOptionsAndCheckIfValid() { // 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"); @@ -337,9 +350,29 @@ private void initTaskLineOptionsAndCheckIfValid() { if (this.commandLine.hasOption('i')) { this.id = this.commandLine.getOptionValue('i').toLowerCase(); + DSpaceObject dso; if (!this.id.equalsIgnoreCase("all")) { - HandleService handleService = HandleServiceFactory.getInstance().getHandleService(); - DSpaceObject dso; + // First, try to parse the id as a UUID. If that fails, treat it as a handle. + UUID uuid = null; + try { + uuid = UUID.fromString(id); + } catch (Exception e) { + // It's not a UUID, proceed to treat it as a handle. + } + if (uuid != null) { + try { + dso = dspaceObjectUtils.findDSpaceObject(context, uuid); + if (dso != null) { + // We already resolved an object, return early + return; + } + } catch (SQLException e) { + String error = "SQLException trying to find dso with uuid " + uuid; + super.handler.logError(error); + throw new RuntimeException(error, e); + } + } + // If we get here, the id is not a UUID, so we assume it's a handle. try { dso = handleService.resolveToObject(this.context, id); } catch (SQLException e) { diff --git a/dspace-api/src/main/java/org/dspace/curate/XmlWorkflowCuratorServiceImpl.java b/dspace-api/src/main/java/org/dspace/curate/XmlWorkflowCuratorServiceImpl.java index 00e91ee1fb40..ec32ff92f9a2 100644 --- a/dspace-api/src/main/java/org/dspace/curate/XmlWorkflowCuratorServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/curate/XmlWorkflowCuratorServiceImpl.java @@ -140,13 +140,14 @@ public boolean curate(Curator curator, Context c, XmlWorkflowItem wfi) item.setOwningCollection(wfi.getCollection()); for (Task task : step.tasks) { curator.addTask(task.name); - // Check whether the task is configured to be queued rather than automatically run - if (StringUtils.isNotEmpty(step.queue)) { - // queue attribute has been set in the FlowStep configuration: add task to configured queue - curator.queue(c, item.getID().toString(), step.queue); - } else { - // Task is configured to be run automatically - curator.curate(c, item); + } + + if (StringUtils.isNotEmpty(step.queue)) { // Step's tasks are to be queued. + curator.queue(c, item.getID().toString(), step.queue); + } else { // Step's tasks are to be run now. + curator.curate(c, item); + + for (Task task : step.tasks) { int status = curator.getStatus(task.name); String result = curator.getResult(task.name); String action = "none"; @@ -183,14 +184,14 @@ public boolean curate(Curator curator, Context c, XmlWorkflowItem wfi) } } curator.clear(); - } - // Record any reporting done by the tasks. - if (reporter.length() > 0) { - LOG.info("Curation tasks over item {} for step {} report:%n{}", - () -> wfi.getItem().getID(), - () -> step.step, - () -> reporter.toString()); + // Record any reporting done by the tasks. + if (reporter.length() > 0) { + LOG.info("Curation tasks over item {} for step {} report:\n{}", + () -> wfi.getItem().getID(), + () -> step.step, + () -> reporter.toString()); + } } } return true; diff --git a/dspace-api/src/main/java/org/dspace/curate/package-info.java b/dspace-api/src/main/java/org/dspace/curate/package-info.java index 492642f60c57..1168bbd283d2 100644 --- a/dspace-api/src/main/java/org/dspace/curate/package-info.java +++ b/dspace-api/src/main/java/org/dspace/curate/package-info.java @@ -20,6 +20,8 @@ * * *

Curation requests may be run immediately or queued for batch processing. + * See {@link TaskQueue} and its relatives, {@link Curation} and its relatives + * for more on queued curation. * *

Tasks may also be attached to a workflow step, so that a set of tasks is * applied to each uninstalled Item which passes through that step. See @@ -27,5 +29,15 @@ * *

A task may return to the Curator a status code, a final status message, * and an optional report character stream. + * + *

The {@link Reporter} classes absorb strings of text and preserve it in + * various ways. A Reporter is a simple {@link Appendable} and makes no + * assumptions about e.g. whether a string represents a complete line. If you + * want your report formatted, insert appropriate newlines and other whitespace + * as needed. Your tasks can emit marked-up text if you wish, but the stock + * Reporter implementations make no attempt to render it. + * + *

Tasks may be annotated to inform the Curator of special properties. See + * {@link Distributive}, {@link Mutative}, {@link Suspendable} etc. */ package org.dspace.curate; diff --git a/dspace-api/src/main/java/org/dspace/discovery/DiscoverResult.java b/dspace-api/src/main/java/org/dspace/discovery/DiscoverResult.java index 00236d2bfe32..a56804e3e7ea 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/DiscoverResult.java +++ b/dspace-api/src/main/java/org/dspace/discovery/DiscoverResult.java @@ -32,6 +32,9 @@ public class DiscoverResult { private List indexableObjects; private Map> 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 void setTotalSearchResults(long totalSearchResults) { this.totalSearchResults = totalSearchResults; } + public long getTotalEntries() { + return totalEntries; + } + + public void setTotalEntries(long totalEntries) { + this.totalEntries = totalEntries; + } + public int getStart() { return start; } diff --git a/dspace-api/src/main/java/org/dspace/discovery/IndexClient.java b/dspace-api/src/main/java/org/dspace/discovery/IndexClient.java index b70e9162f7a1..3479c25bf367 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/IndexClient.java +++ b/dspace-api/src/main/java/org/dspace/discovery/IndexClient.java @@ -27,6 +27,7 @@ import org.dspace.content.Item; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.ItemService; +import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.discovery.indexobject.IndexableCollection; import org.dspace.discovery.indexobject.IndexableCommunity; @@ -109,7 +110,7 @@ public void internalRun() throws Exception { .getHandleService().resolveToObject(context, param); if (dso != null) { final IndexFactory indexableObjectService = IndexObjectFactoryFactory.getInstance(). - getIndexFactoryByType(String.valueOf(dso.getType())); + getIndexFactoryByType(Constants.typeText[dso.getType()]); indexableObject = indexableObjectService.findIndexableObject(context, dso.getID().toString()); } } diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrSearchCore.java b/dspace-api/src/main/java/org/dspace/discovery/SolrSearchCore.java index 6304f39a8ca9..57c56dc35348 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrSearchCore.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrSearchCore.java @@ -88,9 +88,11 @@ protected void initSolr() { solrServer.setBaseURL(solrService); solrServer.setUseMultiPartPost(true); // Dummy/test query to search for Item (type=2) of ID=1 - SolrQuery solrQuery = new SolrQuery() - .setQuery(SearchUtils.RESOURCE_TYPE_FIELD + ":" + IndexableItem.TYPE + - " AND " + SearchUtils.RESOURCE_ID_FIELD + ":1"); + SolrQuery solrQuery = new SolrQuery(); + solrQuery.setQuery("*:*"); + solrQuery.addFilterQuery( + SearchUtils.RESOURCE_TYPE_FIELD + ":" + IndexableItem.TYPE, + SearchUtils.RESOURCE_ID_FIELD + ":1"); // Only return obj identifier fields in result doc solrQuery.setFields(SearchUtils.RESOURCE_TYPE_FIELD, SearchUtils.RESOURCE_ID_FIELD); solrServer.query(solrQuery, REQUEST_METHOD); diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java index 7aece5acf313..6142dd0dba4b 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java @@ -52,21 +52,23 @@ public void additionalIndex(Context context, IndexableObject indexableObject, So List bitstreams = bundle.getBitstreams(); if (bitstreams != null) { for (Bitstream bitstream : bitstreams) { - document.addField(SOLR_FIELD_NAME_FOR_FILENAMES, bitstream.getName()); - // Add _keyword and _filter fields which are necessary to support filtering and faceting - // for the file names - document.addField(SOLR_FIELD_NAME_FOR_FILENAMES + "_keyword", bitstream.getName()); - document.addField(SOLR_FIELD_NAME_FOR_FILENAMES + "_filter", bitstream.getName()); + if (bitstream != null) { + document.addField(SOLR_FIELD_NAME_FOR_FILENAMES, bitstream.getName()); + // Add _keyword and _filter fields which are necessary to + // support filtering and faceting for the file names + document.addField(SOLR_FIELD_NAME_FOR_FILENAMES + "_keyword", bitstream.getName()); + document.addField(SOLR_FIELD_NAME_FOR_FILENAMES + "_filter", bitstream.getName()); - String description = bitstream.getDescription(); - if ((description != null) && !description.isEmpty()) { - document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS, description); - // Add _keyword and _filter fields which are necessary to support filtering and - // faceting for the descriptions - document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS + "_keyword", - description); - document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS + "_filter", - description); + String description = bitstream.getDescription(); + if ((description != null) && !description.isEmpty()) { + document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS, description); + // Add _keyword and _filter fields which are necessary to support filtering and + // faceting for the descriptions + document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS + "_keyword", + description); + document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS + "_filter", + description); + } } } } diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java index a0c1188d7132..a5fa04b3dc04 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java @@ -41,6 +41,9 @@ 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.BucketJsonFacet; +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; @@ -72,6 +75,7 @@ import org.dspace.discovery.indexobject.IndexableItem; import org.dspace.discovery.indexobject.factory.IndexFactory; import org.dspace.discovery.indexobject.factory.IndexObjectFactoryFactory; +import org.dspace.discovery.indexobject.factory.ItemIndexFactory; import org.dspace.eperson.Group; import org.dspace.eperson.factory.EPersonServiceFactory; import org.dspace.eperson.service.GroupService; @@ -341,6 +345,7 @@ public void updateIndex(Context context, boolean force, String type) { try { final List indexableObjectServices = indexObjectServiceFactory. getIndexFactories(); + int indexObject = 0; for (IndexFactory indexableObjectService : indexableObjectServices) { if (type == null || StringUtils.equals(indexableObjectService.getType(), type)) { final Iterator indexableObjects = indexableObjectService.findAll(context); @@ -348,6 +353,10 @@ public void updateIndex(Context context, boolean force, String type) { final IndexableObject indexableObject = indexableObjects.next(); indexContent(context, indexableObject, force); context.uncacheEntity(indexableObject.getIndexedObject()); + indexObject++; + if ((indexObject % 100) == 0 && indexableObjectService instanceof ItemIndexFactory) { + context.uncacheEntities(); + } } } } @@ -852,16 +861,20 @@ protected SolrQuery resolveToSolrQuery(Context context, DiscoverQuery discoveryQ solrQuery.setQuery(query); - // Add any search fields to our query. This is the limited list - // of fields that will be returned in the solr result - for (String fieldName : discoveryQuery.getSearchFields()) { - solrQuery.addField(fieldName); + if (discoveryQuery.getMaxResults() != 0) { + // set search fields in Solr query only if we are interested in the actual search results + + // Add any search fields to our query. This is the limited list + // of fields that will be returned in the solr result + for (String fieldName : discoveryQuery.getSearchFields()) { + solrQuery.addField(fieldName); + } + // Also ensure a few key obj identifier fields are returned with every query + solrQuery.addField(SearchUtils.RESOURCE_TYPE_FIELD); + solrQuery.addField(SearchUtils.RESOURCE_ID_FIELD); + solrQuery.addField(SearchUtils.RESOURCE_UNIQUE_ID); + solrQuery.addField(STATUS_FIELD); } - // Also ensure a few key obj identifier fields are returned with every query - solrQuery.addField(SearchUtils.RESOURCE_TYPE_FIELD); - solrQuery.addField(SearchUtils.RESOURCE_ID_FIELD); - solrQuery.addField(SearchUtils.RESOURCE_UNIQUE_ID); - solrQuery.addField(STATUS_FIELD); if (discoveryQuery.isSpellCheck()) { solrQuery.setParam(SpellingParams.SPELLCHECK_Q, query); @@ -1055,6 +1068,8 @@ protected DiscoverResult retrieveResult(Context context, DiscoverQuery query) } //Resolve our facet field values resolveFacetFields(context, query, result, skipLoadingResponse, solrQueryResponse); + //Resolve our json facet field values used for metadata browsing + resolveJsonFacetFields(context, 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 +1095,42 @@ protected DiscoverResult retrieveResult(Context context, DiscoverQuery query) return result; } + /** + * Process the 'json.facet' response, which is currently only used for metadata browsing + * + * @param context context object + * @param result the result object to add the facet results to + * @param solrQueryResponse the solr query response + * @throws SQLException if database error + */ + private void resolveJsonFacetFields(Context context, DiscoverResult result, QueryResponse solrQueryResponse) + throws SQLException { + NestableJsonFacet response = solrQueryResponse.getJsonFacetingResponse(); + if (response != null && response.getBucketBasedFacetNames() != null) { + for (String facetName : response.getBucketBasedFacetNames()) { + BucketBasedJsonFacet facet = response.getBucketBasedFacets(facetName); + if (facet != null) { + result.setTotalEntries(facet.getNumBucketsCount()); + for (BucketJsonFacet bucket : facet.getBuckets()) { + String facetValue = bucket.getVal() != null ? bucket.getVal().toString() : ""; + String field = facetName + "_filter"; + String displayedValue = transformDisplayedValue(context, field, facetValue); + String authorityValue = transformAuthorityValue(context, field, facetValue); + String sortValue = transformSortValue(context, field, facetValue); + String filterValue = displayedValue; + if (StringUtils.isNotBlank(authorityValue)) { + filterValue = authorityValue; + } + result.addFacetResult(facetName, + new DiscoverResult.FacetResult(filterValue, displayedValue, + authorityValue, sortValue, bucket.getCount(), + DiscoveryConfigurationParameters.TYPE_TEXT)); + } + } + } + } + } private void resolveFacetFields(Context context, DiscoverQuery query, DiscoverResult result, boolean skipLoadingResponse, QueryResponse solrQueryResponse) throws SQLException { @@ -1411,8 +1461,6 @@ protected String transformFacetField(DiscoverFacetField facetFieldConfig, String } else { return field + "_acid"; } - } else if (facetFieldConfig.getType().equals(DiscoveryConfigurationParameters.TYPE_STANDARD)) { - return field; } else { return field; } diff --git a/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryConfigurationService.java b/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryConfigurationService.java index 6cb93e2993f3..9d603941de39 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryConfigurationService.java +++ b/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryConfigurationService.java @@ -10,8 +10,10 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -197,15 +199,19 @@ public List getIndexAlwaysConfigurations() { } /** - * @return All configurations for {@link org.dspace.discovery.configuration.DiscoverySearchFilterFacet} + * Get the unique set of configured Discovery facets. This is used when inspecting configuration + * to include hierarchical vocabularies in the browse menu. + * + * @return All unique instances of {@link org.dspace.discovery.configuration.DiscoverySearchFilterFacet} + * included in "sidebarFacets" bean, across all Discovery configurations. */ - public List getAllFacetsConfig() { - List configs = new ArrayList<>(); + public List getAllUniqueFacetsConfig() { + Set configs = new LinkedHashSet<>(); for (String key : map.keySet()) { DiscoveryConfiguration config = map.get(key); configs.addAll(config.getSidebarFacets()); } - return configs; + return new ArrayList<>(configs); } public static void main(String[] args) { diff --git a/dspace-api/src/main/java/org/dspace/discovery/indexobject/IndexFactoryImpl.java b/dspace-api/src/main/java/org/dspace/discovery/indexobject/IndexFactoryImpl.java index f1ae137b9163..c9a865ec85b2 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/indexobject/IndexFactoryImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/indexobject/IndexFactoryImpl.java @@ -118,20 +118,10 @@ protected void writeDocument(SolrInputDocument doc, FullTextContentStreams strea 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 @@ protected void writeDocument(SolrInputDocument doc, FullTextContentStreams strea // 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 @@ protected void writeDocument(SolrInputDocument doc, FullTextContentStreams strea } 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); diff --git a/dspace-api/src/main/java/org/dspace/discovery/indexobject/ItemIndexFactoryImpl.java b/dspace-api/src/main/java/org/dspace/discovery/indexobject/ItemIndexFactoryImpl.java index 7cdb8b93d80e..a7a755749604 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/indexobject/ItemIndexFactoryImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/indexobject/ItemIndexFactoryImpl.java @@ -67,8 +67,6 @@ 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 SolrInputDocument buildDocument(Context context, IndexableItem indexableI 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 SolrInputDocument buildDocument(Context context, IndexableItem indexableI 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 List getIndexableObjects(Context context, Item item) throws SQLException 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)); } diff --git a/dspace-api/src/main/java/org/dspace/discovery/indexobject/LDNMessageEntityIndexFactoryImpl.java b/dspace-api/src/main/java/org/dspace/discovery/indexobject/LDNMessageEntityIndexFactoryImpl.java index 7752ae58627f..218f49f62c79 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/indexobject/LDNMessageEntityIndexFactoryImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/indexobject/LDNMessageEntityIndexFactoryImpl.java @@ -124,7 +124,9 @@ public SolrInputDocument buildDocument(Context context, IndexableLDNNotification doc.addField("type", ldnMessage.getType()); addFacetIndex(doc, "activity_stream_type", ldnMessage.getActivityStreamType(), ldnMessage.getActivityStreamType()); - addFacetIndex(doc, "coar_notify_type", ldnMessage.getCoarNotifyType(), ldnMessage.getCoarNotifyType()); + if (ldnMessage.getCoarNotifyType() != null) { + addFacetIndex(doc, "coar_notify_type", ldnMessage.getCoarNotifyType(), ldnMessage.getCoarNotifyType()); + } doc.addField("queue_attempts", ldnMessage.getQueueAttempts()); doc.addField("queue_attempts_sort", ldnMessage.getQueueAttempts()); diff --git a/dspace-api/src/main/java/org/dspace/discovery/utils/DiscoverQueryBuilder.java b/dspace-api/src/main/java/org/dspace/discovery/utils/DiscoverQueryBuilder.java index 92a973dff883..b816e222539a 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/utils/DiscoverQueryBuilder.java +++ b/dspace-api/src/main/java/org/dspace/discovery/utils/DiscoverQueryBuilder.java @@ -302,7 +302,7 @@ private void configureSorting(String sortProperty, String sortDirection, Discove if (StringUtils.isNotBlank(sortBy) && !isConfigured(sortBy, searchSortConfiguration)) { throw new SearchServiceException( - "The field: " + sortBy + "is not configured for the configuration!"); + "The field: " + sortBy + " is not configured for the configuration!"); } diff --git a/dspace-api/src/main/java/org/dspace/eperson/CaptchaServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/CaptchaServiceImpl.java index b213675b163e..15f92247d6cc 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/CaptchaServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/CaptchaServiceImpl.java @@ -17,15 +17,15 @@ import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.annotation.PostConstruct; -import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; -import org.apache.http.client.HttpClient; 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.HttpClientBuilder; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.message.BasicNameValuePair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.eperson.service.CaptchaService; import org.dspace.services.ConfigurationService; import org.springframework.beans.factory.annotation.Autowired; @@ -82,18 +82,17 @@ public void processResponse(String response, String action) throws InvalidReCapt throw new RuntimeException(e.getMessage(), e); } - HttpClient httpClient = HttpClientBuilder.create().build(); - HttpResponse httpResponse; - GoogleCaptchaResponse googleResponse; - final ObjectMapper objectMapper = new ObjectMapper(); - try { - httpResponse = httpClient.execute(httpPost); - googleResponse = objectMapper.readValue(httpResponse.getEntity().getContent(), GoogleCaptchaResponse.class); + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { + final ObjectMapper objectMapper = new ObjectMapper(); + try (CloseableHttpResponse httpResponse = httpClient.execute(httpPost)) { + GoogleCaptchaResponse googleResponse = objectMapper.readValue(httpResponse.getEntity().getContent(), + GoogleCaptchaResponse.class); + validateGoogleResponse(googleResponse, action); + } } catch (IOException e) { log.error(e.getMessage(), e); throw new RuntimeException("Error during verify google recaptcha site", e); } - validateGoogleResponse(googleResponse, action); } private boolean responseSanityCheck(String response) { diff --git a/dspace-api/src/main/java/org/dspace/eperson/Group2GroupCache.java b/dspace-api/src/main/java/org/dspace/eperson/Group2GroupCache.java index a1c12371f5ff..0c6ea58b1977 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/Group2GroupCache.java +++ b/dspace-api/src/main/java/org/dspace/eperson/Group2GroupCache.java @@ -15,6 +15,7 @@ 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 @@ * @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 diff --git a/dspace-api/src/main/java/org/dspace/eperson/GroupServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/GroupServiceImpl.java index 730053e42ce2..4cec4c9c0d93 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/GroupServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/GroupServiceImpl.java @@ -20,6 +20,7 @@ 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; @@ -147,7 +148,7 @@ public void addMember(Context context, Group group, EPerson e) { public void addMember(Context context, Group groupParent, Group groupChild) throws SQLException { // don't add if it's already a member // and don't add itself - if (groupParent.contains(groupChild) || groupParent.getID() == groupChild.getID()) { + if (groupParent.contains(groupChild) || groupParent.getID().equals(groupChild.getID())) { return; } @@ -178,7 +179,7 @@ public void removeMember(Context context, Group group, EPerson ePerson) throws S Role role = stepByName.getRole(); for (CollectionRole collectionRole : collectionRoles) { if (StringUtils.equals(collectionRole.getRoleId(), role.getId()) - && claimedTask.getWorkflowItem().getCollection() == collectionRole.getCollection()) { + && claimedTask.getWorkflowItem().getCollection().equals(collectionRole.getCollection())) { // Count number of EPersons who are *direct* members of this group int totalDirectEPersons = ePersonService.countByGroups(context, Set.of(group)); // Count number of Groups which have this groupParent as a direct parent @@ -673,15 +674,14 @@ protected boolean isEPersonInGroup(Context context, Group group, EPerson ePerson /** - * 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> computeNewCache(Context context, boolean flushQueries) throws SQLException { Map> parents = new HashMap<>(); List> group2groupResults = groupDAO.getGroup2GroupResults(context, flushQueries); @@ -689,19 +689,8 @@ protected void rethinkGroupCache(Context context, boolean flushQueries) throws S UUID parent = group2groupResult.getLeft(); UUID child = group2groupResult.getRight(); - // if parent doesn't have an entry, create one - if (!parents.containsKey(parent)) { - Set 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 children = parents.get(parent); - children.add(child); - } + parents.putIfAbsent(parent, new HashSet<>()); + parents.get(parent).add(child); } // now parents is a hash of all of the IDs of groups that are parents @@ -714,27 +703,42 @@ protected void rethinkGroupCache(Context context, boolean flushQueries) throws S 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> newCache = new HashSet<>(); for (Map.Entry> parent : parents.entrySet()) { UUID key = parent.getKey(); - for (UUID child : parent.getValue()) { + newCache.add(Pair.of(key, child)); + } + } + return newCache; + } - Group parentGroup = find(context, key); - Group childGroup = find(context, child); + /** + * 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> oldCache = group2GroupCacheDAO.getCache(context); - 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); - } - } + // correct cache, computed from the Group table + Set> newCache = computeNewCache(context, flushQueries); + + SetUtils.SetView> toDelete = SetUtils.difference(oldCache, newCache); + SetUtils.SetView> toCreate = SetUtils.difference(newCache, oldCache); + + for (Pair pair : toDelete ) { + group2GroupCacheDAO.deleteFromCache(context, pair.getLeft(), pair.getRight()); + } + + for (Pair pair : toCreate ) { + group2GroupCacheDAO.addToCache(context, pair.getLeft(), pair.getRight()); } } diff --git a/dspace-api/src/main/java/org/dspace/eperson/SubscribeServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/SubscribeServiceImpl.java index 2e4d94f4431e..0f5d2ba319cd 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/SubscribeServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/SubscribeServiceImpl.java @@ -131,7 +131,8 @@ public List findAvailableSubscriptions(Context context, EPerson eper @Override public boolean isSubscribed(Context context, EPerson eperson, DSpaceObject dSpaceObject) throws SQLException { - return subscriptionDAO.findByEPersonAndDso(context, eperson, dSpaceObject, -1, -1) != null; + List subscriptions = subscriptionDAO.findByEPersonAndDso(context, eperson, dSpaceObject, -1, -1); + return subscriptions != null && !subscriptions.isEmpty(); } @Override diff --git a/dspace-api/src/main/java/org/dspace/eperson/dao/Group2GroupCacheDAO.java b/dspace-api/src/main/java/org/dspace/eperson/dao/Group2GroupCacheDAO.java index 7db569a59e2b..d41d52c7e618 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/dao/Group2GroupCacheDAO.java +++ b/dspace-api/src/main/java/org/dspace/eperson/dao/Group2GroupCacheDAO.java @@ -9,7 +9,10 @@ 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 @@ */ public interface Group2GroupCacheDAO extends GenericDAO { - public List 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> getCache(Context context) throws SQLException; - public List findByChildren(Context context, Iterable 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 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 findByChildren(Context context, Iterable 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; } diff --git a/dspace-api/src/main/java/org/dspace/eperson/dao/impl/Group2GroupCacheDAOImpl.java b/dspace-api/src/main/java/org/dspace/eperson/dao/impl/Group2GroupCacheDAOImpl.java index 1cd359188ca3..adbd776ffab6 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/dao/impl/Group2GroupCacheDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/dao/impl/Group2GroupCacheDAOImpl.java @@ -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 @@ protected Group2GroupCacheDAOImpl() { super(); } + @Override + public Set> 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> results = query.getResultList(); + return new HashSet>(results); + } + @Override public List findByParent(Context context, Group group) throws SQLException { CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); @@ -90,4 +104,24 @@ public Group2GroupCache find(Context context, Group parent, Group child) throws 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(); + } } diff --git a/dspace-api/src/main/java/org/dspace/external/OpenaireRestConnector.java b/dspace-api/src/main/java/org/dspace/external/OpenaireRestConnector.java index 27688df6c758..87af01401ac0 100644 --- a/dspace-api/src/main/java/org/dspace/external/OpenaireRestConnector.java +++ b/dspace-api/src/main/java/org/dspace/external/OpenaireRestConnector.java @@ -27,13 +27,13 @@ import org.apache.http.NameValuePair; import org.apache.http.NoHttpResponseException; import org.apache.http.StatusLine; -import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; -import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.message.BasicNameValuePair; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.app.util.Util; import org.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; @@ -120,33 +120,34 @@ public OpenaireRestToken grabNewAccessToken() throws IOException { params.add(new BasicNameValuePair("grant_type", "client_credentials")); httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8")); - HttpClient httpClient = HttpClientBuilder.create().build(); - HttpResponse getResponse = httpClient.execute(httpPost); - - JSONObject responseObject = null; - try (InputStream is = getResponse.getEntity().getContent(); - BufferedReader streamReader = new BufferedReader(new InputStreamReader(is, "UTF-8"))) { - String inputStr; - // verify if we have basic json - while ((inputStr = streamReader.readLine()) != null && responseObject == null) { - if (inputStr.startsWith("{") && inputStr.endsWith("}") && inputStr.contains("access_token") - && inputStr.contains("expires_in")) { - try { - responseObject = new JSONObject(inputStr); - } catch (Exception e) { - // Not as valid as I'd hoped, move along - responseObject = null; + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { + HttpResponse getResponse = httpClient.execute(httpPost); + + JSONObject responseObject = null; + try (InputStream is = getResponse.getEntity().getContent(); + BufferedReader streamReader = new BufferedReader(new InputStreamReader(is, "UTF-8"))) { + String inputStr; + // verify if we have basic json + while ((inputStr = streamReader.readLine()) != null && responseObject == null) { + if (inputStr.startsWith("{") && inputStr.endsWith("}") && inputStr.contains("access_token") + && inputStr.contains("expires_in")) { + try { + responseObject = new JSONObject(inputStr); + } catch (Exception e) { + // Not as valid as I'd hoped, move along + responseObject = null; + } } } } - } - if (responseObject == null || !responseObject.has("access_token") || !responseObject.has("expires_in")) { - throw new IOException("Unable to grab the access token using provided service url, client id and secret"); - } - - return new OpenaireRestToken(responseObject.get("access_token").toString(), - Long.valueOf(responseObject.get("expires_in").toString())); + if (responseObject == null || !responseObject.has("access_token") || !responseObject.has("expires_in")) { + throw new IOException("Unable to grab the access token using provided service url, " + + "client id and secret"); + } + return new OpenaireRestToken(responseObject.get("access_token").toString(), + Long.valueOf(responseObject.get("expires_in").toString())); + } } /** @@ -171,42 +172,43 @@ public InputStream get(String file, String accessToken) { httpGet.addHeader("Authorization", "Bearer " + accessToken); } - HttpClient httpClient = HttpClientBuilder.create().build(); - getResponse = httpClient.execute(httpGet); - - StatusLine status = getResponse.getStatusLine(); - - // registering errors - switch (status.getStatusCode()) { - case HttpStatus.SC_NOT_FOUND: - // 404 - Not found - case HttpStatus.SC_FORBIDDEN: - // 403 - Invalid Access Token - case 429: - // 429 - Rate limit abuse for unauthenticated user - Header[] limitUsed = getResponse.getHeaders("x-ratelimit-used"); - Header[] limitMax = getResponse.getHeaders("x-ratelimit-limit"); - - if (limitUsed.length > 0) { - String limitMsg = limitUsed[0].getValue(); - if (limitMax.length > 0) { - limitMsg = limitMsg.concat(" of " + limitMax[0].getValue()); + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { + getResponse = httpClient.execute(httpGet); + + StatusLine status = getResponse.getStatusLine(); + + // registering errors + switch (status.getStatusCode()) { + case HttpStatus.SC_NOT_FOUND: + // 404 - Not found + case HttpStatus.SC_FORBIDDEN: + // 403 - Invalid Access Token + case 429: + // 429 - Rate limit abuse for unauthenticated user + Header[] limitUsed = getResponse.getHeaders("x-ratelimit-used"); + Header[] limitMax = getResponse.getHeaders("x-ratelimit-limit"); + + if (limitUsed.length > 0) { + String limitMsg = limitUsed[0].getValue(); + if (limitMax.length > 0) { + limitMsg = limitMsg.concat(" of " + limitMax[0].getValue()); + } + getGotError(new NoHttpResponseException(status.getReasonPhrase() + " with usage limit " + + limitMsg), + url + '/' + file); + } else { + // 429 - Rate limit abuse + getGotError(new NoHttpResponseException(status.getReasonPhrase()), url + '/' + file); } - getGotError( - new NoHttpResponseException(status.getReasonPhrase() + " with usage limit " + limitMsg), - url + '/' + file); - } else { - // 429 - Rate limit abuse - getGotError(new NoHttpResponseException(status.getReasonPhrase()), url + '/' + file); - } - break; - default: - // 200 or other - break; - } + break; + default: + // 200 or other + break; + } - // do not close this httpClient - result = getResponse.getEntity().getContent(); + // do not close this httpClient + result = getResponse.getEntity().getContent(); + } } catch (MalformedURLException e1) { getGotError(e1, url + '/' + file); } catch (Exception e) { diff --git a/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java b/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java index d45be7e6b56e..aa16af7a524d 100644 --- a/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java +++ b/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java @@ -7,17 +7,18 @@ */ package org.dspace.external; +import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Scanner; import org.apache.commons.lang3.StringUtils; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; /** * @author Antoine Snyers (antoine at atmire.com) @@ -39,7 +40,7 @@ public OrcidRestConnector(String url) { } public InputStream get(String path, String accessToken) { - HttpResponse getResponse = null; + CloseableHttpResponse getResponse = null; InputStream result = null; path = trimSlashes(path); @@ -49,11 +50,13 @@ public InputStream get(String path, String accessToken) { httpGet.addHeader("Content-Type", "application/vnd.orcid+xml"); httpGet.addHeader("Authorization","Bearer " + accessToken); } - try { - HttpClient httpClient = HttpClientBuilder.create().build(); + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { getResponse = httpClient.execute(httpGet); - //do not close this httpClient - result = getResponse.getEntity().getContent(); + try (InputStream responseStream = getResponse.getEntity().getContent()) { + // Read all the content of the response stream into a byte array to prevent TruncatedChunkException + byte[] content = responseStream.readAllBytes(); + result = new ByteArrayInputStream(content); + } } catch (Exception e) { getGotError(e, fullPath); } diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java index 125da8f7c67b..a9e10f92948d 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java @@ -7,24 +7,17 @@ */ package org.dspace.external.provider.impl; -import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.LinkedList; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.content.dto.MetadataValueDTO; @@ -32,8 +25,9 @@ import org.dspace.external.model.ExternalDataObject; import org.dspace.external.provider.AbstractExternalDataProvider; import org.dspace.external.provider.orcid.xml.XMLtoBio; -import org.json.JSONObject; +import org.dspace.orcid.model.factory.OrcidFactoryUtils; import org.orcid.jaxb.model.v3.release.common.OrcidIdentifier; +import org.orcid.jaxb.model.v3.release.record.Email; import org.orcid.jaxb.model.v3.release.record.Person; import org.orcid.jaxb.model.v3.release.search.Result; import org.springframework.beans.factory.annotation.Autowired; @@ -60,6 +54,11 @@ public class OrcidV3AuthorDataProvider extends AbstractExternalDataProvider { private XMLtoBio converter; + /** + * Maximum retries to allow for the access token retrieval + */ + private int maxClientRetries = 3; + public static final String ORCID_ID_SYNTAX = "\\d{4}-\\d{4}-\\d{4}-(\\d{3}X|\\d{4})"; private static final int MAX_INDEX = 10000; @@ -78,47 +77,37 @@ public OrcidV3AuthorDataProvider() { * @throws java.io.IOException passed through from HTTPclient. */ public void init() throws IOException { - if (StringUtils.isNotBlank(clientSecret) && StringUtils.isNotBlank(clientId) - && StringUtils.isNotBlank(OAUTHUrl)) { - String authenticationParameters = "?client_id=" + clientId + - "&client_secret=" + clientSecret + - "&scope=/read-public&grant_type=client_credentials"; - HttpPost httpPost = new HttpPost(OAUTHUrl + authenticationParameters); - httpPost.addHeader("Accept", "application/json"); - httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); - - HttpClient httpClient = HttpClientBuilder.create().build(); - HttpResponse getResponse = httpClient.execute(httpPost); - - JSONObject responseObject = null; - try (InputStream is = getResponse.getEntity().getContent(); - BufferedReader streamReader = new BufferedReader(new InputStreamReader(is, "UTF-8"))) { - String inputStr; - while ((inputStr = streamReader.readLine()) != null && responseObject == null) { - if (inputStr.startsWith("{") && inputStr.endsWith("}") && inputStr.contains("access_token")) { - try { - responseObject = new JSONObject(inputStr); - } catch (Exception e) { - //Not as valid as I'd hoped, move along - responseObject = null; - } - } - } - } - if (responseObject != null && responseObject.has("access_token")) { - accessToken = (String) responseObject.get("access_token"); - } + // Initialize access token at spring instantiation. If it fails, the access token will be null rather + // than causing a fatal Spring startup error + initializeAccessToken(); + } + + /** + * Initialize access token, logging an error and decrementing remaining retries if an IOException is thrown. + * If the optional access token result is empty, set to null instead. + */ + public void initializeAccessToken() { + // If we have reaches max retries or the access token is already set, return immediately + if (maxClientRetries <= 0 || StringUtils.isNotBlank(accessToken)) { + return; + } + try { + accessToken = OrcidFactoryUtils.retrieveAccessToken(clientId, clientSecret, OAUTHUrl).orElse(null); + } catch (IOException e) { + log.error("Error retrieving ORCID access token, {} retries left", --maxClientRetries); } } @Override public Optional getExternalDataObject(String id) { + initializeAccessToken(); Person person = getBio(id); ExternalDataObject externalDataObject = convertToExternalDataObject(person); return Optional.of(externalDataObject); } protected ExternalDataObject convertToExternalDataObject(Person person) { + initializeAccessToken(); ExternalDataObject externalDataObject = new ExternalDataObject(sourceIdentifier); if (person.getName() != null) { String lastName = ""; @@ -126,13 +115,20 @@ protected ExternalDataObject convertToExternalDataObject(Person person) { if (person.getName().getFamilyName() != null) { lastName = person.getName().getFamilyName().getContent(); externalDataObject.addMetadata(new MetadataValueDTO("person", "familyName", null, null, - lastName)); + lastName)); } if (person.getName().getGivenNames() != null) { firstName = person.getName().getGivenNames().getContent(); externalDataObject.addMetadata(new MetadataValueDTO("person", "givenName", null, null, - firstName)); - + firstName)); + } + if (person.getEmails().getEmails() != null && !person.getEmails().getEmails().isEmpty()) { + Email email = person.getEmails().getEmails().get(0); + if (person.getEmails().getEmails().size() > 1) { + email = person.getEmails().getEmails().stream().filter(Email::isPrimary).findFirst().orElse(email); + } + externalDataObject.addMetadata(new MetadataValueDTO("person", "email", null, + null, email.getEmail())); } externalDataObject.setId(person.getName().getPath()); externalDataObject @@ -140,7 +136,7 @@ protected ExternalDataObject convertToExternalDataObject(Person person) { new MetadataValueDTO("person", "identifier", "orcid", null, person.getName().getPath())); externalDataObject .addMetadata(new MetadataValueDTO("dc", "identifier", "uri", null, - orcidUrl + "/" + person.getName().getPath())); + orcidUrl + "/" + person.getName().getPath())); if (!StringUtils.isBlank(lastName) && !StringUtils.isBlank(firstName)) { externalDataObject.setDisplayValue(lastName + ", " + firstName); externalDataObject.setValue(lastName + ", " + firstName); @@ -151,8 +147,8 @@ protected ExternalDataObject convertToExternalDataObject(Person person) { externalDataObject.setDisplayValue(firstName); externalDataObject.setValue(firstName); } - } else if (person.getPath() != null ) { - externalDataObject.setId(StringUtils.substringBetween(person.getPath(),"/","/person")); + } else if (person.getPath() != null) { + externalDataObject.setId(StringUtils.substringBetween(person.getPath(), "/", "/person")); } return externalDataObject; } @@ -167,14 +163,13 @@ public Person getBio(String id) { if (!isValid(id)) { return null; } - InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), accessToken); - Person person = converter.convertSinglePerson(bioDocument); - try { - bioDocument.close(); - } catch (IOException e) { - log.error(e.getMessage(), e); + if (orcidRestConnector == null) { + log.error("ORCID REST connector is null, returning null ORCID Person Bio"); + return null; } - return person; + initializeAccessToken(); + InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), accessToken); + return converter.convertSinglePerson(bioDocument); } /** @@ -188,12 +183,18 @@ private boolean isValid(String text) { @Override public List searchExternalDataObjects(String query, int start, int limit) { + initializeAccessToken(); if (limit > 100) { throw new IllegalArgumentException("The maximum number of results to retrieve cannot exceed 100."); } if (start > MAX_INDEX) { throw new IllegalArgumentException("The starting number of results to retrieve cannot exceed 10000."); } + // Check REST connector is initialized + if (orcidRestConnector == null) { + log.error("ORCID REST connector is not initialized, returning empty list"); + return Collections.emptyList(); + } String searchPath = "search?q=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&start=" + start @@ -205,7 +206,7 @@ public List searchExternalDataObjects(String query, int star for (Result result : results) { OrcidIdentifier orcidIdentifier = result.getOrcidIdentifier(); if (orcidIdentifier != null) { - log.debug("Found OrcidId=" + orcidIdentifier.toString()); + log.debug("Found OrcidId=" + orcidIdentifier.getPath()); String orcid = orcidIdentifier.getPath(); Person bio = getBio(orcid); if (bio != null) { @@ -213,14 +214,6 @@ public List searchExternalDataObjects(String query, int star } } } - try { - bioDocument.close(); - } catch (IOException e) { - log.error(e.getMessage(), e); - } - if (Objects.isNull(bios)) { - return Collections.emptyList(); - } return bios.stream().map(bio -> convertToExternalDataObject(bio)).collect(Collectors.toList()); } @@ -231,6 +224,11 @@ public boolean supports(String source) { @Override public int getNumberOfResults(String query) { + if (orcidRestConnector == null) { + log.error("ORCID REST connector is null, returning 0"); + return 0; + } + initializeAccessToken(); String searchPath = "search?q=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&start=" + 0 + "&rows=" + 0; diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/SHERPAv2JournalDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/SHERPAv2JournalDataProvider.java index a4276c83ed70..f0ce6a979a1f 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/SHERPAv2JournalDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/SHERPAv2JournalDataProvider.java @@ -97,7 +97,7 @@ private ExternalDataObject constructExternalDataObjectFromSherpaJournal(SHERPAJo if (CollectionUtils.isNotEmpty(sherpaJournal.getIssns())) { String issn = sherpaJournal.getIssns().get(0); externalDataObject.addMetadata(new MetadataValueDTO( - "dc", "identifier", "issn", null, issn)); + "creativeworkseries", "issn", null, null, issn)); } diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/SHERPAv2JournalISSNDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/SHERPAv2JournalISSNDataProvider.java index 9e61b9ac2ac0..860334847ccb 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/SHERPAv2JournalISSNDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/SHERPAv2JournalISSNDataProvider.java @@ -106,8 +106,7 @@ private ExternalDataObject constructExternalDataObjectFromSherpaJournal(SHERPAJo String issn = sherpaJournal.getIssns().get(0); externalDataObject.setId(issn); externalDataObject.addMetadata(new MetadataValueDTO( - "dc", "identifier", "issn", null, issn)); - + "creativeworkseries", "issn", null, null, issn)); } log.debug("New external data object. Title=" + externalDataObject.getValue() + ". ID=" diff --git a/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/Converter.java b/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/Converter.java index 578db6c56749..ab641c00042b 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/Converter.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/Converter.java @@ -16,6 +16,7 @@ import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Unmarshaller; +import org.dspace.app.util.XMLUtils; import org.xml.sax.SAXException; /** @@ -31,9 +32,7 @@ public abstract class Converter { protected Object unmarshall(InputStream input, Class type) throws SAXException, URISyntaxException { try { - XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); - // disallow DTD parsing to ensure no XXE attacks can occur - xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); + XMLInputFactory xmlInputFactory = XMLUtils.getXMLInputFactory(); XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(input); JAXBContext context = JAXBContext.newInstance(type); diff --git a/dspace-api/src/main/java/org/dspace/external/service/impl/ExternalDataServiceImpl.java b/dspace-api/src/main/java/org/dspace/external/service/impl/ExternalDataServiceImpl.java index 59cbe4f9d087..b241f5d782db 100644 --- a/dspace-api/src/main/java/org/dspace/external/service/impl/ExternalDataServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/external/service/impl/ExternalDataServiceImpl.java @@ -108,8 +108,9 @@ public WorkspaceItem createWorkspaceItemFromExternalDataObject(Context context, } log.info(LogHelper.getHeader(context, "create_item_from_externalDataObject", "Created item" + - "with id: " + item.getID() + " from source: " + externalDataObject.getSource() + " with identifier: " + - externalDataObject.getId())); + " with id: " + item.getID() + + " from source: " + externalDataObject.getSource() + + " with identifier: " + externalDataObject.getId())); try { List providers = suggestionService.getSuggestionProviders(); if (providers != null) { diff --git a/dspace-api/src/main/java/org/dspace/google/GoogleAccount.java b/dspace-api/src/main/java/org/dspace/google/GoogleAccount.java deleted file mode 100644 index a24c02a2e1c3..000000000000 --- a/dspace-api/src/main/java/org/dspace/google/GoogleAccount.java +++ /dev/null @@ -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 scopes = new HashSet(); - 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; - } - -} - diff --git a/dspace-api/src/main/java/org/dspace/google/GoogleQueryManager.java b/dspace-api/src/main/java/org/dspace/google/GoogleQueryManager.java deleted file mode 100644 index 2719aef04da4..000000000000 --- a/dspace-api/src/main/java/org/dspace/google/GoogleQueryManager.java +++ /dev/null @@ -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(); - } - -} - diff --git a/dspace-api/src/main/java/org/dspace/google/GoogleRecorderEventListener.java b/dspace-api/src/main/java/org/dspace/google/GoogleRecorderEventListener.java deleted file mode 100644 index fb4e9c04de5b..000000000000 --- a/dspace-api/src/main/java/org/dspace/google/GoogleRecorderEventListener.java +++ /dev/null @@ -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 nvps = new ArrayList(); - 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); - } - -} diff --git a/dspace-api/src/main/java/org/dspace/google/client/GoogleAnalyticsClientImpl.java b/dspace-api/src/main/java/org/dspace/google/client/GoogleAnalyticsClientImpl.java index 915bd25b065f..bfecb1fb0861 100644 --- a/dspace-api/src/main/java/org/dspace/google/client/GoogleAnalyticsClientImpl.java +++ b/dspace-api/src/main/java/org/dspace/google/client/GoogleAnalyticsClientImpl.java @@ -18,9 +18,9 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.google.GoogleAnalyticsEvent; /** @@ -42,7 +42,7 @@ public class GoogleAnalyticsClientImpl implements GoogleAnalyticsClient { public GoogleAnalyticsClientImpl(String keyPrefix, GoogleAnalyticsClientRequestBuilder requestBuilder) { this.keyPrefix = keyPrefix; this.requestBuilder = requestBuilder; - this.httpclient = HttpClients.createDefault(); + this.httpclient = DSpaceHttpClientFactory.getInstance().build(); } @Override diff --git a/dspace-api/src/main/java/org/dspace/identifier/IdentifierServiceImpl.java b/dspace-api/src/main/java/org/dspace/identifier/IdentifierServiceImpl.java index b98aea24fa08..e6dcfcdda693 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/IdentifierServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/identifier/IdentifierServiceImpl.java @@ -57,6 +57,11 @@ public void setProviders(List providers) { } } + @Override + public List getProviders() { + return this.providers; + } + /** * Reserves identifiers for the item * diff --git a/dspace-api/src/main/java/org/dspace/identifier/VersionedDOIIdentifierProvider.java b/dspace-api/src/main/java/org/dspace/identifier/VersionedDOIIdentifierProvider.java index e5a90907c7b6..b42c915163dc 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/VersionedDOIIdentifierProvider.java +++ b/dspace-api/src/main/java/org/dspace/identifier/VersionedDOIIdentifierProvider.java @@ -354,7 +354,10 @@ void removePreviousVersionDOIsOutOfObject(Context c, Item item, String oldDoi) 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 occured.", ex); diff --git a/dspace-api/src/main/java/org/dspace/identifier/VersionedHandleIdentifierProviderWithCanonicalHandles.java b/dspace-api/src/main/java/org/dspace/identifier/VersionedHandleIdentifierProviderWithCanonicalHandles.java index 51458b4ad6e3..6f44fd332eb5 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/VersionedHandleIdentifierProviderWithCanonicalHandles.java +++ b/dspace-api/src/main/java/org/dspace/identifier/VersionedHandleIdentifierProviderWithCanonicalHandles.java @@ -15,11 +15,14 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Collection; +import org.dspace.content.Community; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; import org.dspace.content.MetadataSchemaEnum; import org.dspace.content.MetadataValue; -import org.dspace.content.service.ItemService; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.DSpaceObjectService; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.core.LogHelper; @@ -62,9 +65,6 @@ public class VersionedHandleIdentifierProviderWithCanonicalHandles extends Ident @Autowired(required = true) private HandleService handleService; - @Autowired(required = true) - private ItemService itemService; - /** * After all the properties are set check that the versioning is enabled * @@ -171,6 +171,16 @@ public String register(Context context, DSpaceObject dso) { throw new RuntimeException("The current user is not authorized to change this item.", ex); } } + if (dso instanceof Collection || dso instanceof Community) { + try { + // Update the metadata with the handle for collections and communities. + modifyHandleMetadata(context, dso, getCanonical(id)); + } catch (SQLException ex) { + throw new RuntimeException("A problem with the database connection occured.", ex); + } catch (AuthorizeException ex) { + throw new RuntimeException("The current user is not authorized to change this item.", ex); + } + } return id; } @@ -489,27 +499,29 @@ protected String getCanonical(String identifier) { * Remove all handles from an item's metadata and add the supplied handle instead. * * @param context The relevant DSpace Context. - * @param item which item to modify + * @param dso which dso to modify * @param handle which handle to add * @throws SQLException if database error * @throws AuthorizeException if authorization error */ - protected void modifyHandleMetadata(Context context, Item item, String handle) + protected void modifyHandleMetadata(Context context, DSpaceObject dso, String handle) throws SQLException, AuthorizeException { // we want to exchange the old handle against the new one. To do so, we // load all identifiers, clear the metadata field, re add all // identifiers which are not from type handle and add the new handle. String handleref = handleService.getCanonicalForm(handle); - List identifiers = itemService - .getMetadata(item, MetadataSchemaEnum.DC.getName(), "identifier", "uri", Item.ANY); - itemService.clearMetadata(context, item, MetadataSchemaEnum.DC.getName(), "identifier", "uri", Item.ANY); + DSpaceObjectService dSpaceObjectService = + ContentServiceFactory.getInstance().getDSpaceObjectService(dso); + List identifiers = dSpaceObjectService + .getMetadata(dso, MetadataSchemaEnum.DC.getName(), "identifier", "uri", Item.ANY); + dSpaceObjectService.clearMetadata(context, dso, MetadataSchemaEnum.DC.getName(), "identifier", "uri", Item.ANY); for (MetadataValue identifier : identifiers) { if (this.supports(identifier.getValue())) { // ignore handles continue; } - itemService.addMetadata(context, - item, + dSpaceObjectService.addMetadata(context, + dso, identifier.getMetadataField(), identifier.getLanguage(), identifier.getValue(), @@ -517,9 +529,9 @@ protected void modifyHandleMetadata(Context context, Item item, String handle) identifier.getConfidence()); } if (!StringUtils.isEmpty(handleref)) { - itemService.addMetadata(context, item, MetadataSchemaEnum.DC.getName(), + dSpaceObjectService.addMetadata(context, dso, MetadataSchemaEnum.DC.getName(), "identifier", "uri", null, handleref); } - itemService.update(context, item); + dSpaceObjectService.update(context, dso); } } diff --git a/dspace-api/src/main/java/org/dspace/identifier/doi/DOIOrganiser.java b/dspace-api/src/main/java/org/dspace/identifier/doi/DOIOrganiser.java index 78507a0edf13..bc73cf8dcf11 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/doi/DOIOrganiser.java +++ b/dspace-api/src/main/java/org/dspace/identifier/doi/DOIOrganiser.java @@ -13,7 +13,6 @@ 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 static void runCLI(Context context, DOIOrganiser organiser, String[] args } 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 static void runCLI(Context context, DOIOrganiser organiser, String[] args + "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 static void runCLI(Context context, DOIOrganiser organiser, String[] args } 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 static void runCLI(Context context, DOIOrganiser organiser, String[] args + "that could be deleted."); } - Iterator 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 void list(String processName, PrintStream out, PrintStream err, Integer . /** * 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."); @@ -421,28 +448,28 @@ public void register(DOI doiRow, Filter filter) throws SQLException, DOIIdentifi + " is successfully registered."); } } catch (IdentifierException ex) { + String message; if (!(ex instanceof DOIIdentifierException)) { - LOG.error("It wasn't possible to register this identifier: " - + DOI.SCHEME + doiRow.getDoi() - + " online. ", ex); + message = "It wasn't possible to register this identifier: " + + DOI.SCHEME + doiRow.getDoi() + + " online. "; + } else { + DOIIdentifierException doiIdentifierException = (DOIIdentifierException) ex; + message = "It wasn't possible to register this identifier : " + + DOI.SCHEME + doiRow.getDoi() + + " online. Exceptions code: " + + DOIIdentifierException.codeToString(doiIdentifierException.getCode()); } - DOIIdentifierException doiIdentifierException = (DOIIdentifierException) ex; - try { sendAlertMail("Register", dso, DOI.SCHEME + doiRow.getDoi(), - doiIdentifierException.codeToString(doiIdentifierException - .getCode())); + message); } catch (IOException ioe) { LOG.error("Couldn't send mail", ioe); } - LOG.error("It wasn't possible to register this identifier : " - + DOI.SCHEME + doiRow.getDoi() - + " online. Exceptions code: " - + doiIdentifierException - .codeToString(doiIdentifierException.getCode()), ex); + LOG.error(message, ex); if (!quiet) { System.err.println("It wasn't possible to register this identifier: " @@ -473,30 +500,33 @@ public void register(DOI doiRow, Filter filter) throws SQLException, DOIIdentifi } /** - * 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(); @@ -511,27 +541,27 @@ public void reserve(DOI doiRow, Filter filter) { System.out.println("This identifier : " + DOI.SCHEME + doiRow.getDoi() + " is successfully reserved."); } } catch (IdentifierException ex) { + String message; if (!(ex instanceof DOIIdentifierException)) { - LOG.error("It wasn't possible to register this identifier : " - + DOI.SCHEME + doiRow.getDoi() - + " online. ", ex); + message = "It wasn't possible to register this identifier : " + + DOI.SCHEME + doiRow.getDoi() + + " online. "; + } else { + DOIIdentifierException doiIdentifierException = (DOIIdentifierException) ex; + message = "It wasn't possible to reserve the identifier online. " + + " Exceptions code: " + + DOIIdentifierException.codeToString(doiIdentifierException.getCode()); } - DOIIdentifierException doiIdentifierException = (DOIIdentifierException) ex; - try { sendAlertMail("Reserve", dso, DOI.SCHEME + doiRow.getDoi(), - DOIIdentifierException.codeToString( - doiIdentifierException.getCode())); + message); } catch (IOException ioe) { LOG.error("Couldn't send mail", ioe); } - LOG.error("It wasn't possible to reserve the identifier online. " - + " Exceptions code: " - + DOIIdentifierException - .codeToString(doiIdentifierException.getCode()), ex); + LOG.error(message, ex); if (!quiet) { System.err.println("It wasn't possible to reserve this identifier: " + DOI.SCHEME + doiRow.getDoi()); @@ -576,26 +606,27 @@ public void update(DOI doiRow) { + doiRow.getDoi() + "."); } } catch (IdentifierException ex) { + String message; if (!(ex instanceof DOIIdentifierException)) { - LOG.error("It wasn't possible to register the identifier online. ", ex); + message = String.format("Registering DOI %s for object %s: the registrar returned an error.", + doiRow.getDoi(), dso.getID()); + } else { + DOIIdentifierException doiIdentifierException = (DOIIdentifierException) ex; + message = "It wasn't possible to update this identifier: " + + DOI.SCHEME + doiRow.getDoi() + + " Exceptions code: " + + DOIIdentifierException.codeToString(doiIdentifierException.getCode()); } - DOIIdentifierException doiIdentifierException = (DOIIdentifierException) ex; - try { sendAlertMail("Update", dso, DOI.SCHEME + doiRow.getDoi(), - doiIdentifierException.codeToString(doiIdentifierException - .getCode())); + message); } catch (IOException ioe) { LOG.error("Couldn't send mail", ioe); } - LOG.error("It wasn't possible to update this identifier: " - + DOI.SCHEME + doiRow.getDoi() - + " Exceptions code: " - + doiIdentifierException - .codeToString(doiIdentifierException.getCode()), ex); + LOG.error(message, ex); if (!quiet) { System.err.println("It wasn't possible to update this identifier: " + DOI.SCHEME + doiRow.getDoi()); @@ -799,4 +830,4 @@ private void setQuiet() { this.quiet = true; } -} \ No newline at end of file +} diff --git a/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java b/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java index 931f1538583e..23853979be05 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java +++ b/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java @@ -7,6 +7,10 @@ */ package org.dspace.identifier.doi; +import static org.dspace.identifier.DOIIdentifierProvider.DOI_ELEMENT; +import static org.dspace.identifier.DOIIdentifierProvider.DOI_QUALIFIER; +import static org.dspace.identifier.DOIIdentifierProvider.MD_SCHEMA; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URISyntaxException; @@ -15,6 +19,7 @@ import java.util.Iterator; import java.util.Map; +import org.apache.commons.lang.StringUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.StatusLine; @@ -31,10 +36,11 @@ import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.crosswalk.CrosswalkException; @@ -383,6 +389,10 @@ public void reserveDOI(Context context, DSpaceObject dso, String doi) parameters.put("hostinginstitution", configurationService.getProperty(CFG_HOSTINGINSTITUTION)); } + parameters.put("mdSchema", MD_SCHEMA); + parameters.put("mdElement", DOI_ELEMENT); + // Pass an empty string for qualifier if the metadata field doesn't have any + parameters.put("mdQualifier", DOI_QUALIFIER); Element root = null; try { @@ -410,7 +420,7 @@ public void reserveDOI(Context context, DSpaceObject dso, String doi) } String metadataDOI = extractDOI(root); - if (null == metadataDOI) { + if (StringUtils.isBlank(metadataDOI)) { // The DOI will be saved as metadata of dso after successful // registration. To register a doi it has to be part of the metadata // sent to DataCite. So we add it to the XML we'll send to DataCite @@ -452,6 +462,10 @@ public void reserveDOI(Context context, DSpaceObject dso, String doi) 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); @@ -623,6 +637,14 @@ protected DataCiteResponse sendGetRequest(String doi, String path) 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(); @@ -631,6 +653,14 @@ protected DataCiteResponse sendMetadataPostRequest(String doi, Element metadataR 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/ @@ -678,7 +708,7 @@ protected DataCiteResponse sendMetadataPostRequest(String doi, String metadata) * 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 { @@ -690,7 +720,7 @@ protected DataCiteResponse sendHttpRequest(HttpUriRequest req, String doi) httpContext.setCredentialsProvider(credentialsProvider); HttpEntity entity = null; - try ( CloseableHttpClient httpclient = HttpClientBuilder.create().build(); ) { + try (CloseableHttpClient httpclient = DSpaceHttpClientFactory.getInstance().build()) { HttpResponse response = httpclient.execute(req, httpContext); StatusLine status = response.getStatusLine(); @@ -800,7 +830,7 @@ protected String extractAlternateIdentifier(Context context, String content) } // parse the XML - SAXBuilder saxBuilder = new SAXBuilder(); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document doc = null; try { doc = saxBuilder.build(new ByteArrayInputStream(content.getBytes("UTF-8"))); diff --git a/dspace-api/src/main/java/org/dspace/identifier/doi/package-info.java b/dspace-api/src/main/java/org/dspace/identifier/doi/package-info.java index e92170daf0cf..30ee5c45dd33 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/doi/package-info.java +++ b/dspace-api/src/main/java/org/dspace/identifier/doi/package-info.java @@ -6,17 +6,14 @@ * http://www.dspace.org/license/ */ /** - * Make requests to the DOI registration angencies, f.e.to - * EZID DOI service, and analyze the responses. - * + * Make requests to the DOI registration agencies and analyze the responses. + * + *

+ * {@link DOIOrganiser} is a tool for managing DOI registrations. + * *

- * 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). - *

+ * Classes specific to the DataCite + * registrar are here. See {@link org.dspace.identifier.ezid} for the + * EZID registrar. */ package org.dspace.identifier.doi; diff --git a/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDRequest.java b/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDRequest.java index bf46c3bf59da..c3c95be4b364 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDRequest.java +++ b/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDRequest.java @@ -26,9 +26,9 @@ import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.identifier.DOI; import org.dspace.identifier.IdentifierException; @@ -87,7 +87,7 @@ public class EZIDRequest { this.authority = authority; } - client = HttpClientBuilder.create().build(); + client = DSpaceHttpClientFactory.getInstance().build(); httpContext = HttpClientContext.create(); if (null != username) { URI uri = new URI(scheme, host, path, null); @@ -124,7 +124,7 @@ public class EZIDRequest { this.authority = authority; } - client = HttpClientBuilder.create().build(); + client = DSpaceHttpClientFactory.getInstance().build(); httpContext = HttpClientContext.create(); if (null != username) { CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); diff --git a/dspace-api/src/main/java/org/dspace/identifier/ezid/package-info.java b/dspace-api/src/main/java/org/dspace/identifier/ezid/package-info.java new file mode 100644 index 000000000000..bff0bdea26e0 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/identifier/ezid/package-info.java @@ -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. + * + *

+ * 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). + *

+ */ +package org.dspace.identifier.ezid; diff --git a/dspace-api/src/main/java/org/dspace/identifier/service/IdentifierService.java b/dspace-api/src/main/java/org/dspace/identifier/service/IdentifierService.java index 23005b657508..45bf3c6dea8a 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/service/IdentifierService.java +++ b/dspace-api/src/main/java/org/dspace/identifier/service/IdentifierService.java @@ -19,6 +19,7 @@ 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 @@ void register(Context context, DSpaceObject dso, String identifier) 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 getProviders(); } diff --git a/dspace-api/src/main/java/org/dspace/iiif/IIIFApiQueryServiceImpl.java b/dspace-api/src/main/java/org/dspace/iiif/IIIFApiQueryServiceImpl.java index 7c6336ed3c7f..ccb2c170d949 100644 --- a/dspace-api/src/main/java/org/dspace/iiif/IIIFApiQueryServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/iiif/IIIFApiQueryServiceImpl.java @@ -12,12 +12,14 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.content.Bitstream; import org.dspace.iiif.util.IIIFSharedUtils; @@ -35,14 +37,10 @@ public class IIIFApiQueryServiceImpl implements IIIFApiQueryService { public int[] getImageDimensions(Bitstream bitstream) { int[] arr = new int[2]; String path = IIIFSharedUtils.getInfoJsonPath(bitstream); - URL url; BufferedReader in = null; - try { - url = new URL(path); - HttpURLConnection con = (HttpURLConnection) url.openConnection(); - con.setRequestMethod("GET"); - in = new BufferedReader( - new InputStreamReader(con.getInputStream())); + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { + CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(path)); + in = new BufferedReader(new InputStreamReader(httpResponse.getEntity().getContent())); String inputLine; StringBuilder response = new StringBuilder(); while ((inputLine = in.readLine()) != null) { diff --git a/dspace-api/src/main/java/org/dspace/importer/external/MultipleParallelImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/MultipleParallelImportMetadataSourceServiceImpl.java index 1bb7e9269596..1ab5319d4066 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/MultipleParallelImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/MultipleParallelImportMetadataSourceServiceImpl.java @@ -17,7 +17,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; -import jakarta.el.MethodNotFoundException; import org.dspace.content.Item; import org.dspace.importer.external.datamodel.ImportRecord; import org.dspace.importer.external.datamodel.Query; @@ -144,7 +143,7 @@ public Collection getRecords(Query query) throws MetadataSourceExc @Override public ImportRecord getRecord(Query query) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for multiple external data sources"); + throw new UnsupportedOperationException("This method is not implemented for multiple external data sources"); } @Override diff --git a/dspace-api/src/main/java/org/dspace/importer/external/ads/ADSImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/ads/ADSImportMetadataSourceServiceImpl.java index 4c72c46732b7..ca3f48da6114 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/ads/ADSImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/ads/ADSImportMetadataSourceServiceImpl.java @@ -21,7 +21,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.el.MethodNotFoundException; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; @@ -99,7 +98,7 @@ public Collection findMatchingRecords(Query query) throws Metadata @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for CrossRef"); + throw new UnsupportedOperationException("This method is not implemented for CrossRef"); } @Override diff --git a/dspace-api/src/main/java/org/dspace/importer/external/arxiv/service/ArXivImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/arxiv/service/ArXivImportMetadataSourceServiceImpl.java index a1df4a7f40c1..a790c6a9a544 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/arxiv/service/ArXivImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/arxiv/service/ArXivImportMetadataSourceServiceImpl.java @@ -15,7 +15,6 @@ import java.util.List; import java.util.concurrent.Callable; -import jakarta.el.MethodNotFoundException; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.client.Invocation; @@ -23,6 +22,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; +import org.dspace.app.util.XMLUtils; import org.dspace.content.Item; import org.dspace.importer.external.datamodel.ImportRecord; import org.dspace.importer.external.datamodel.Query; @@ -162,7 +162,7 @@ public String getImportSource() { @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { // FIXME: we need this method? - throw new MethodNotFoundException("This method is not implemented for ArXiv"); + throw new UnsupportedOperationException("This method is not implemented for ArXiv"); } /** @@ -219,7 +219,7 @@ public Integer call() throws Exception { if (response.getStatus() == 200) { String responseString = response.readEntity(String.class); - SAXBuilder saxBuilder = new SAXBuilder(); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(responseString)); Element root = document.getRootElement(); @@ -400,7 +400,7 @@ private String getQuery(Query query) { private List splitToRecords(String recordsSrc) { try { - SAXBuilder saxBuilder = new SAXBuilder(); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(recordsSrc)); Element root = document.getRootElement(); diff --git a/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiImportMetadataSourceServiceImpl.java index 66572f9a3d16..41c80ab7fe64 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiImportMetadataSourceServiceImpl.java @@ -20,13 +20,13 @@ import java.util.Objects; import java.util.concurrent.Callable; -import jakarta.el.MethodNotFoundException; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.apache.http.HttpException; import org.apache.http.client.utils.URIBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.content.Item; import org.dspace.importer.external.datamodel.ImportRecord; import org.dspace.importer.external.datamodel.Query; @@ -113,7 +113,7 @@ public Collection findMatchingRecords(Query query) throws Metadata @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for Cinii"); + throw new UnsupportedOperationException("This method is not implemented for Cinii"); } public String getUrl() { @@ -302,9 +302,7 @@ protected List search(String id, String appId) private List splitToRecords(String recordsSrc) { try { - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(recordsSrc)); Element root = document.getRootElement(); return root.getChildren(); @@ -357,9 +355,7 @@ private List getCiniiIds(String appId, Integer maxResult, String author, Map> params = new HashMap>(); String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); int url_len = this.url.length() - 1; - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(response)); Element root = document.getRootElement(); List namespaces = Arrays.asList( @@ -421,9 +417,7 @@ private Integer countCiniiElement(String appId, Integer maxResult, String author Map> params = new HashMap>(); String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(response)); Element root = document.getRootElement(); List namespaces = Arrays @@ -450,4 +444,4 @@ private MetadatumDTO createIdentifier(String id) { return metadatumDTO; } -} \ No newline at end of file +} diff --git a/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAbstractProcessor.java b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAbstractProcessor.java index 1b6da9d37b16..99f1ee37a54e 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAbstractProcessor.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAbstractProcessor.java @@ -12,7 +12,6 @@ import java.util.ArrayList; import java.util.Collection; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import com.fasterxml.jackson.core.JsonProcessingException; @@ -21,6 +20,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.importer.external.metadatamapping.contributor.JsonPathMetadataProcessor; import org.w3c.dom.Document; import org.w3c.dom.Node; @@ -64,10 +64,9 @@ private String prettifyAbstract(String abstractValue) { } String xmlString = "" + abstractValue + ""; - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); Document xmlDoc; try { - DocumentBuilder builder = factory.newDocumentBuilder(); + DocumentBuilder builder = XMLUtils.getDocumentBuilder(); InputSource is = new InputSource(new StringReader(xmlString)); xmlDoc = builder.parse(is); } catch (SAXException | IOException | ParserConfigurationException e) { diff --git a/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAuthorMetadataProcessor.java b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAuthorMetadataProcessor.java index abf84f52d058..b9b384f8ed77 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAuthorMetadataProcessor.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAuthorMetadataProcessor.java @@ -42,8 +42,8 @@ public Collection processMetadata(String json) { JsonNode author = authors.next(); String givenName = author.at("/given").textValue(); String familyName = author.at("/family").textValue(); - if (StringUtils.isNoneBlank(givenName) && StringUtils.isNoneBlank(familyName)) { - values.add(givenName + " " + familyName); + if (StringUtils.isNotBlank(givenName) && StringUtils.isNotBlank(familyName)) { + values.add(familyName.trim() + ", " + givenName.trim()); } } return values; @@ -64,4 +64,4 @@ public void setPathToArray(String pathToArray) { this.pathToArray = pathToArray; } -} \ No newline at end of file +} diff --git a/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefImportMetadataSourceServiceImpl.java index 37e613d9c5ff..88aeec8d9498 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefImportMetadataSourceServiceImpl.java @@ -20,7 +20,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.el.MethodNotFoundException; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; @@ -112,7 +111,7 @@ public Collection findMatchingRecords(Query query) throws Metadata @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for CrossRef"); + throw new UnsupportedOperationException("This method is not implemented for CrossRef"); } public String getID(String id) { diff --git a/dspace-api/src/main/java/org/dspace/importer/external/datacite/DataCiteImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/datacite/DataCiteImportMetadataSourceServiceImpl.java index ad6e260bd0d5..34405cc3eec4 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/datacite/DataCiteImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/datacite/DataCiteImportMetadataSourceServiceImpl.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.el.MethodNotFoundException; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -188,7 +187,7 @@ public Collection findMatchingRecords(Query query) throws Metadata @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for DataCite"); + throw new UnsupportedOperationException("This method is not implemented for DataCite"); } public String getID(String query) { diff --git a/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java index fbae302bca6a..552f607827a8 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java @@ -32,6 +32,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.xerces.impl.dv.util.Base64; +import org.dspace.app.util.XMLUtils; import org.dspace.content.Item; import org.dspace.importer.external.datamodel.ImportRecord; import org.dspace.importer.external.datamodel.Query; @@ -397,9 +398,11 @@ private Integer countDocument(String bearer, String query) { String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); + // To properly parse EPO responses, we must allow DOCTYPEs overall. But, we can still apply all the + // other default XXE protections, including disabling external entities and entity expansion. + // NOTE: we only need to allow DOCTYPEs for this initial API call. All other calls have them disabled. + saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false); Document document = saxBuilder.build(new StringReader(response)); Element root = document.getRootElement(); @@ -436,9 +439,7 @@ private List searchDocumentIds(String bearer, String query, int s String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(response)); Element root = document.getRootElement(); @@ -489,9 +490,7 @@ private List searchDocument(String bearer, String id, String docTy private List splitToRecords(String recordsSrc) { try { - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(recordsSrc)); Element root = document.getRootElement(); List namespaces = Arrays.asList(Namespace.getNamespace("ns", "http://www.epo.org/exchange")); diff --git a/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClientImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClientImpl.java index 1a8a7a7861ed..987f2fde34dc 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClientImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClientImpl.java @@ -17,19 +17,16 @@ import org.apache.commons.collections.MapUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.http.HttpHost; import org.apache.http.HttpResponse; import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.config.RequestConfig.Builder; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.services.ConfigurationService; import org.springframework.beans.factory.annotation.Autowired; @@ -53,16 +50,15 @@ public class LiveImportClientImpl implements LiveImportClient { @Override public String executeHttpGetRequest(int timeout, String URL, Map> params) { HttpGet method = null; + RequestConfig config = RequestConfig.custom() + .setConnectionRequestTimeout(timeout) + .setConnectTimeout(timeout) + .setSocketTimeout(timeout) + .build(); try (CloseableHttpClient httpClient = Optional.ofNullable(this.httpClient) - .orElseGet(HttpClients::createDefault)) { - - Builder requestConfigBuilder = RequestConfig.custom(); - requestConfigBuilder.setConnectionRequestTimeout(timeout); - RequestConfig defaultRequestConfig = requestConfigBuilder.build(); - + .orElse(DSpaceHttpClientFactory.getInstance().buildWithRequestConfig(config))) { String uri = buildUrl(URL, params.get(URI_PARAMETERS)); method = new HttpGet(uri); - method.setConfig(defaultRequestConfig); Map headerParams = params.get(HEADER_PARAMETERS); if (MapUtils.isNotEmpty(headerParams)) { @@ -71,7 +67,6 @@ public String executeHttpGetRequest(int timeout, String URL, Map> params, String entry) { HttpPost method = null; + RequestConfig config = RequestConfig.custom().build(); try (CloseableHttpClient httpClient = Optional.ofNullable(this.httpClient) - .orElseGet(HttpClients::createDefault)) { - - Builder requestConfigBuilder = RequestConfig.custom(); - RequestConfig defaultRequestConfig = requestConfigBuilder.build(); + .orElse(DSpaceHttpClientFactory.getInstance().buildWithRequestConfig(config))) { String uri = buildUrl(URL, params.get(URI_PARAMETERS)); method = new HttpPost(uri); - method.setConfig(defaultRequestConfig); if (StringUtils.isNotBlank(entry)) { method.setEntity(new StringEntity(entry)); } setHeaderParams(method, params); - configureProxy(method, defaultRequestConfig); if (log.isDebugEnabled()) { log.debug("Performing POST request to \"" + uri + "\"..." ); } @@ -129,17 +120,6 @@ public String executeHttpPostRequest(String URL, Map return StringUtils.EMPTY; } - private void configureProxy(HttpRequestBase method, RequestConfig defaultRequestConfig) { - String proxyHost = configurationService.getProperty("http.proxy.host"); - String proxyPort = configurationService.getProperty("http.proxy.port"); - if (StringUtils.isNotBlank(proxyHost) && StringUtils.isNotBlank(proxyPort)) { - RequestConfig requestConfig = RequestConfig.copy(defaultRequestConfig) - .setProxy(new HttpHost(proxyHost, Integer.parseInt(proxyPort), "http")) - .build(); - method.setConfig(requestConfig); - } - } - /** * Allows to set the header parameters to the HTTP Post method * diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/transform/StringJsonValueMappingMetadataProcessorService.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/transform/StringJsonValueMappingMetadataProcessorService.java new file mode 100644 index 000000000000..1a5e50e4ac39 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/transform/StringJsonValueMappingMetadataProcessorService.java @@ -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.
+ * Like:
+ * journal-article = Article + * + * @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 processMetadata(String json) { + JsonNode rootNode = convertStringJsonToJsonNode(json); + Optional abstractNode = Optional.of(rootNode.at(path)); + Collection 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; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/importer/external/openaire/service/OpenAireImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/openaire/service/OpenAireImportMetadataSourceServiceImpl.java index 0e7bc5e532e8..ea5b7a67617c 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/openaire/service/OpenAireImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/openaire/service/OpenAireImportMetadataSourceServiceImpl.java @@ -15,12 +15,12 @@ import java.util.List; import java.util.concurrent.Callable; -import jakarta.el.MethodNotFoundException; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.client.Invocation; import jakarta.ws.rs.client.WebTarget; import jakarta.ws.rs.core.Response; +import org.dspace.app.util.XMLUtils; import org.dspace.content.Item; import org.dspace.importer.external.datamodel.ImportRecord; import org.dspace.importer.external.datamodel.Query; @@ -135,12 +135,12 @@ public Collection getRecords(Query query) throws MetadataSourceExc @Override public Collection findMatchingRecords(Query query) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for OpenAIRE"); + throw new UnsupportedOperationException("This method is not implemented for OpenAIRE"); } @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for OpenAIRE"); + throw new UnsupportedOperationException("This method is not implemented for OpenAIRE"); } /** @@ -249,7 +249,7 @@ public Integer call() throws Exception { if (response.getStatus() == 200) { String responseString = response.readEntity(String.class); - SAXBuilder saxBuilder = new SAXBuilder(); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(responseString)); Element root = document.getRootElement(); @@ -331,7 +331,7 @@ private ImportRecord filterMultipleTitles(ImportRecord transformSourceRecords) { private List splitToRecords(String recordsSrc) { try { - SAXBuilder saxBuilder = new SAXBuilder(); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(recordsSrc)); Element root = document.getRootElement(); diff --git a/dspace-api/src/main/java/org/dspace/importer/external/pubmed/metadatamapping/contributor/PubmedAbstractMetadatumContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/metadatamapping/contributor/PubmedAbstractMetadatumContributor.java new file mode 100644 index 000000000000..727d6b92ae35 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/metadatamapping/contributor/PubmedAbstractMetadatumContributor.java @@ -0,0 +1,67 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.importer.external.pubmed.metadatamapping.contributor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.dspace.importer.external.metadatamapping.contributor.SimpleXpathMetadatumContributor; +import org.jdom2.Element; +import org.jdom2.Namespace; +import org.jdom2.filter.Filters; +import org.jdom2.xpath.XPathExpression; +import org.jdom2.xpath.XPathFactory; + +/** + * This class is responsible for extracting the abstract from a PubMed XML document. + * It uses XPath to find the relevant elements and constructs a formatted string for the abstract, respecting + * PubMed's labelled abstract format, and including the labels in the output. + */ +public class PubmedAbstractMetadatumContributor extends SimpleXpathMetadatumContributor { + + @Override + public Collection contributeMetadata(Element t) { + List values = new LinkedList<>(); + + List namespaces = new ArrayList<>(); + for (String ns : prefixToNamespaceMapping.keySet()) { + namespaces.add(Namespace.getNamespace(prefixToNamespaceMapping.get(ns), ns)); + } + + XPathExpression xpath = XPathFactory.instance().compile(query, Filters.element(), null, namespaces); + List nodes = xpath.evaluate(t); + StringBuilder sb = new StringBuilder(); + + for (Element el : nodes) { + String label = el.getAttributeValue("Label"); + String text = el.getTextNormalize(); + + if (text == null || text.isEmpty()) { + continue; + } + + if (sb.length() > 0) { + sb.append("\n\n"); + } + + if (label != null && !label.equalsIgnoreCase("UNLABELLED")) { + sb.append(label).append(": "); + } + sb.append(text); + } + + String fullAbstract = sb.toString().trim(); + if (!fullAbstract.isEmpty()) { + values.add(metadataFieldMapping.toDCValue(field, fullAbstract)); + } + return values; + } +} diff --git a/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java index a6cfa625bbcf..c870161bf9bd 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java @@ -24,6 +24,7 @@ import com.google.common.io.CharStreams; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; +import org.dspace.app.util.XMLUtils; import org.dspace.content.Item; import org.dspace.importer.external.datamodel.ImportRecord; import org.dspace.importer.external.datamodel.Query; @@ -233,7 +234,10 @@ private String getSingleElementValue(String src, String elementName) { String value = null; try { - SAXBuilder saxBuilder = new SAXBuilder(); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); + // To properly parse PubMed responses, we must allow DOCTYPEs overall. But, we can still apply all the + // other default XXE protections, including disabling external entities and entity expansion. + saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false); Document document = saxBuilder.build(new StringReader(src)); Element root = document.getRootElement(); @@ -350,12 +354,10 @@ public Collection call() throws Exception { private List splitToRecords(String recordsSrc) { try { - SAXBuilder saxBuilder = new SAXBuilder(); - // Disallow external entities & entity expansion to protect against XXE attacks - // (NOTE: We receive errors if we disable all DTDs for PubMed, so this is the best we can do) - saxBuilder.setFeature("http://xml.org/sax/features/external-general-entities", false); - saxBuilder.setFeature("http://xml.org/sax/features/external-parameter-entities", false); - saxBuilder.setExpandEntities(false); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); + // To properly parse PubMed responses, we must allow DOCTYPEs overall. But, we can still apply all the + // other default XXE protections, including disabling external entities and entity expansion. + saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false); Document document = saxBuilder.build(new StringReader(recordsSrc)); Element root = document.getRootElement(); diff --git a/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeMetadataSourceServiceImpl.java index 5aae8ca8cf50..24f40339ddec 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeMetadataSourceServiceImpl.java @@ -18,13 +18,13 @@ import java.util.Objects; import java.util.concurrent.Callable; -import jakarta.el.MethodNotFoundException; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpException; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.utils.URIBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.content.Item; import org.dspace.importer.external.datamodel.ImportRecord; import org.dspace.importer.external.datamodel.Query; @@ -153,7 +153,7 @@ public Collection findMatchingRecords(Query query) throws Metadata @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for PubMed Europe"); + throw new UnsupportedOperationException("This method is not implemented for PubMed Europe"); } @Override @@ -293,9 +293,7 @@ public Integer count(String query) throws URISyntaxException, ClientProtocolExce Map> params = new HashMap>(); String response = liveImportClient.executeHttpGetRequest(1000, buildURI(1, query), params); - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(response)); Element root = document.getRootElement(); Element element = root.getChild("hitCount"); @@ -366,9 +364,7 @@ public List search(String query, Integer size, Integer start) thro String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); String cursorMark = StringUtils.EMPTY; if (StringUtils.isNotBlank(response)) { - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(response)); XPathFactory xpfac = XPathFactory.instance(); XPathExpression xPath = xpfac.compile("//responseWrapper/resultList/result", @@ -420,4 +416,4 @@ public void setUrl(String url) { this.url = url; } -} \ No newline at end of file +} diff --git a/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java index aa11ac0bb710..8298b6d6f011 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.el.MethodNotFoundException; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; @@ -91,12 +90,12 @@ public ImportRecord getRecord(Query query) throws MetadataSourceException { @Override public Collection findMatchingRecords(Query query) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for ROR"); + throw new UnsupportedOperationException("This method is not implemented for ROR"); } @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for ROR"); + throw new UnsupportedOperationException("This method is not implemented for ROR"); } @Override diff --git a/dspace-api/src/main/java/org/dspace/importer/external/scielo/service/ScieloImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/scielo/service/ScieloImportMetadataSourceServiceImpl.java index ce0c20435ecf..cb988a5e55fb 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/scielo/service/ScieloImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/scielo/service/ScieloImportMetadataSourceServiceImpl.java @@ -21,7 +21,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import jakarta.el.MethodNotFoundException; import jakarta.ws.rs.BadRequestException; import org.apache.commons.collections4.CollectionUtils; import org.apache.http.client.utils.URIBuilder; @@ -99,17 +98,17 @@ public int getRecordsCount(String query) throws MetadataSourceException { @Override public int getRecordsCount(Query query) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for Scielo"); + throw new UnsupportedOperationException("This method is not implemented for Scielo"); } @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for Scielo"); + throw new UnsupportedOperationException("This method is not implemented for Scielo"); } @Override public Collection findMatchingRecords(Query query) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for Scielo"); + throw new UnsupportedOperationException("This method is not implemented for Scielo"); } /** diff --git a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusImportMetadataSourceServiceImpl.java index 39b2be7ad5f8..22e3534ca89f 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusImportMetadataSourceServiceImpl.java @@ -23,10 +23,10 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import jakarta.el.MethodNotFoundException; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.content.Item; import org.dspace.importer.external.datamodel.ImportRecord; import org.dspace.importer.external.datamodel.Query; @@ -152,7 +152,7 @@ public ImportRecord getRecord(Query query) throws MetadataSourceException { @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for Scopus"); + throw new UnsupportedOperationException("This method is not implemented for Scopus"); } @Override @@ -209,9 +209,7 @@ public Integer call() throws Exception { return 0; } - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(response)); Element root = document.getRootElement(); @@ -398,9 +396,7 @@ private Map getRequestParameters(String query, String viewMode, private List splitToRecords(String recordsSrc) { try { - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(recordsSrc)); Element root = document.getRootElement(); String totalResults = root.getChildText("totalResults", Namespace.getNamespace("http://a9.com/-/spec/opensearch/1.1/")); diff --git a/dspace-api/src/main/java/org/dspace/importer/external/vufind/VuFindImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/vufind/VuFindImportMetadataSourceServiceImpl.java index 8933569a060f..7eb3743d207b 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/vufind/VuFindImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/vufind/VuFindImportMetadataSourceServiceImpl.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.el.MethodNotFoundException; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; import org.apache.logging.log4j.LogManager; @@ -104,7 +103,7 @@ public Collection findMatchingRecords(Query query) throws Metadata @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for VuFind"); + throw new UnsupportedOperationException("This method is not implemented for VuFind"); } @Override diff --git a/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java index c7b5aaa49e27..2ac63d50513f 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java @@ -22,11 +22,11 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import jakarta.el.MethodNotFoundException; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.content.Item; import org.dspace.importer.external.datamodel.ImportRecord; import org.dspace.importer.external.datamodel.Query; @@ -57,7 +57,7 @@ public class WOSImportMetadataSourceServiceImpl extends AbstractImportMetadataSo private static final String AI_PATTERN = "^AI=(.*)"; private static final Pattern ISI_PATTERN = Pattern.compile("^\\d{15}$"); - private int timeout = 1000; + private final int timeout = 1000; private String url; private String urlSearch; @@ -109,17 +109,17 @@ public int getRecordsCount(String query) throws MetadataSourceException { @Override public int getRecordsCount(Query query) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for WOS"); + throw new UnsupportedOperationException("This method is not implemented for WOS"); } @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for WOS"); + throw new UnsupportedOperationException("This method is not implemented for WOS"); } @Override public Collection findMatchingRecords(Query query) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for WOS"); + throw new UnsupportedOperationException("This method is not implemented for WOS"); } /** @@ -127,7 +127,7 @@ public Collection findMatchingRecords(Query query) throws Metadata */ private class SearchNBByQueryCallable implements Callable { - private String query; + private final String query; private SearchNBByQueryCallable(String queryString) { this.query = queryString; @@ -146,9 +146,7 @@ public Integer call() throws Exception { params.put(HEADER_PARAMETERS, getRequestParameters()); String response = liveImportClient.executeHttpGetRequest(timeout, url, params); - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(response)); Element root = document.getRootElement(); XPathExpression xpath = XPathFactory.instance().compile("//*[@name=\"RecordsFound\"]", @@ -156,7 +154,8 @@ public Integer call() throws Exception { Element tot = xpath.evaluateFirst(root); return Integer.valueOf(tot.getValue()); } - return null; + log.warn("API key is missing: cannot execute count request."); + return 0; } } @@ -167,7 +166,7 @@ public Integer call() throws Exception { */ private class FindByIdCallable implements Callable> { - private String doi; + private final String doi; private FindByIdCallable(String doi) { this.doi = URLEncoder.encode(doi, StandardCharsets.UTF_8); @@ -186,6 +185,8 @@ public List call() throws Exception { for (Element record : elements) { results.add(transformSourceRecords(record)); } + } else { + log.warn("API key is missing: cannot execute live import request."); } return results; } @@ -203,7 +204,7 @@ public List call() throws Exception { */ private class SearchByQueryCallable implements Callable> { - private Query query; + private final Query query; private SearchByQueryCallable(String queryString, Integer maxResult, Integer start) { query = new Query(); @@ -233,6 +234,8 @@ public List call() throws Exception { for (Element el : omElements) { results.add(transformSourceRecords(el)); } + } else { + log.warn("API key is missing: cannot execute live import request."); } return results; } @@ -271,9 +274,7 @@ private String checkQuery(String query) { } else if (isIsi(query)) { return "UT=(" + query + ")"; } - StringBuilder queryBuilder = new StringBuilder("TS=("); - queryBuilder.append(query).append(")"); - return queryBuilder.toString(); + return "TS=(" + query + ")"; } private boolean isIsi(String query) { @@ -286,9 +287,7 @@ private boolean isIsi(String query) { private List splitToRecords(String recordsSrc) { try { - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(recordsSrc)); Element root = document.getRootElement(); String cData = XPathFactory.instance().compile("//*[@name=\"Records\"]", @@ -330,4 +329,4 @@ public void setApiKey(String apiKey) { this.apiKey = apiKey; } -} \ No newline at end of file +} diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java index cdecadba5242..3c9088bda5df 100644 --- a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java @@ -10,9 +10,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.StringReader; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLConnection; import java.text.MessageFormat; import java.util.Collections; import java.util.HashMap; @@ -28,9 +25,10 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; +import org.dspace.app.util.XMLUtils; import org.dspace.services.ConfigurationService; import org.jdom2.Attribute; import org.jdom2.Document; @@ -53,7 +51,7 @@ public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, private Logger log = org.apache.logging.log4j.LogManager.getLogger(CCLicenseConnectorServiceImpl.class); private CloseableHttpClient client; - protected SAXBuilder parser = new SAXBuilder(); + protected SAXBuilder parser = XMLUtils.getSAXBuilder(); private String postArgument = "answers"; private String postAnswerFormat = @@ -70,12 +68,7 @@ public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, @Override public void afterPropertiesSet() throws Exception { - HttpClientBuilder builder = HttpClientBuilder.create(); - - client = builder - .disableAutomaticRetries() - .setMaxConnTotal(5) - .build(); + client = DSpaceHttpClientFactory.getInstance().buildWithoutAutomaticRetries(5); // disallow DTD parsing to ensure no XXE attacks can occur. // See https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html @@ -333,23 +326,13 @@ private String createAnswerString(final Map parameterMap) { @Override public Document retrieveLicenseRDFDoc(String licenseURI) throws IOException { String ccLicenseUrl = configurationService.getProperty("cc.api.rooturl"); - String issueUrl = ccLicenseUrl + "/details?license-uri=" + licenseURI; - - URL request_url; - try { - request_url = new URL(issueUrl); - } catch (MalformedURLException e) { - return null; - } - URLConnection connection = request_url.openConnection(); - connection.setDoOutput(true); - try { + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { + CloseableHttpResponse httpResponse = httpClient.execute(new HttpPost(issueUrl)); // parsing document from input stream - InputStream stream = connection.getInputStream(); + InputStream stream = httpResponse.getEntity().getContent(); Document doc = parser.build(stream); return doc; - } catch (Exception e) { log.error("Error while retrieving the license document for URI: " + licenseURI, e); } diff --git a/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClient.java b/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClient.java index 99d1920aa53a..d21f61a922f5 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClient.java +++ b/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClient.java @@ -10,6 +10,7 @@ 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); + } diff --git a/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClientImpl.java b/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClientImpl.java index 8356167692e3..11bf444384cd 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClientImpl.java +++ b/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClientImpl.java @@ -35,13 +35,16 @@ import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.NameValuePair; -import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.RequestBuilder; import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.message.BasicNameValuePair; +import org.dspace.app.client.DSpaceHttpClientFactory; +import org.dspace.app.util.XMLUtils; +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 +181,16 @@ public OrcidResponse deleteByPutCode(String accessToken, String orcid, String pu return execute(buildDeleteUriRequest(accessToken, "/" + orcid + path + "/" + putCode), true); } + @Override + public void revokeToken(OrcidToken orcidToken) { + List 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 +233,14 @@ private HttpUriRequest buildPostUriRequest(String accessToken, String relativePa .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,22 +255,34 @@ private HttpUriRequest buildDeleteUriRequest(String accessToken, String relative .build(); } - private T executeAndParseJson(HttpUriRequest httpUriRequest, Class clazz) { - - HttpClient client = HttpClientBuilder.create().build(); - - return executeAndReturns(() -> { - - HttpResponse response = client.execute(httpUriRequest); - + private void executeSuccessful(HttpUriRequest httpUriRequest) { + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { + CloseableHttpResponse response = client.execute(httpUriRequest); if (isNotSuccessfull(response)) { - throw new OrcidClientException(getStatusCode(response), formatErrorMessage(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); + } + } - return objectMapper.readValue(response.getEntity().getContent(), clazz); - - }); - + private T executeAndParseJson(HttpUriRequest httpUriRequest, Class clazz) { + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { + return executeAndReturns(() -> { + CloseableHttpResponse response = client.execute(httpUriRequest); + if (isNotSuccessfull(response)) { + throw new OrcidClientException(getStatusCode(response), formatErrorMessage(response)); + } + return objectMapper.readValue(response.getEntity().getContent(), clazz); + }); + } catch (IOException e) { + throw new RuntimeException(e); + } } /** @@ -264,44 +297,37 @@ private T executeAndParseJson(HttpUriRequest httpUriRequest, Class clazz) * @throws OrcidClientException if the incoming response is not successfull */ private T executeAndUnmarshall(HttpUriRequest httpUriRequest, boolean handleNotFoundAsNull, Class clazz) { - - HttpClient client = HttpClientBuilder.create().build(); - - return executeAndReturns(() -> { - - HttpResponse response = client.execute(httpUriRequest); - - if (handleNotFoundAsNull && isNotFound(response)) { - return null; - } - - if (isNotSuccessfull(response)) { - throw new OrcidClientException(getStatusCode(response), formatErrorMessage(response)); - } - - return unmarshall(response.getEntity(), clazz); - - }); + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { + return executeAndReturns(() -> { + CloseableHttpResponse response = client.execute(httpUriRequest); + if (handleNotFoundAsNull && isNotFound(response)) { + return null; + } + if (isNotSuccessfull(response)) { + throw new OrcidClientException(getStatusCode(response), formatErrorMessage(response)); + } + return unmarshall(response.getEntity(), clazz); + }); + } catch (IOException e) { + throw new RuntimeException(e); + } } private OrcidResponse execute(HttpUriRequest httpUriRequest, boolean handleNotFoundAsNull) { - HttpClient client = HttpClientBuilder.create().build(); - - return executeAndReturns(() -> { - - HttpResponse response = client.execute(httpUriRequest); - - if (handleNotFoundAsNull && isNotFound(response)) { - return new OrcidResponse(getStatusCode(response), null, getContent(response)); - } - - if (isNotSuccessfull(response)) { - throw new OrcidClientException(getStatusCode(response), formatErrorMessage(response)); - } - - return new OrcidResponse(getStatusCode(response), getPutCode(response), getContent(response)); - - }); + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { + return executeAndReturns(() -> { + CloseableHttpResponse response = client.execute(httpUriRequest); + if (handleNotFoundAsNull && isNotFound(response)) { + return new OrcidResponse(getStatusCode(response), null, getContent(response)); + } + if (isNotSuccessfull(response)) { + throw new OrcidClientException(getStatusCode(response), formatErrorMessage(response)); + } + return new OrcidResponse(getStatusCode(response), getPutCode(response), getContent(response)); + }); + } catch (IOException e) { + throw new RuntimeException(e); + } } private T executeAndReturns(ThrowingSupplier supplier) { @@ -326,8 +352,7 @@ private String marshall(Object object) throws JAXBException { @SuppressWarnings("unchecked") private T unmarshall(HttpEntity entity, Class clazz) throws Exception { JAXBContext jaxbContext = JAXBContext.newInstance(clazz); - XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); - xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); + XMLInputFactory xmlInputFactory = XMLUtils.getXMLInputFactory(); XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(entity.getContent()); Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); return (T) unmarshaller.unmarshal(xmlStreamReader); diff --git a/dspace-api/src/main/java/org/dspace/orcid/client/OrcidConfiguration.java b/dspace-api/src/main/java/org/dspace/orcid/client/OrcidConfiguration.java index 550b0215c435..dfa90fcae03a 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/client/OrcidConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/orcid/client/OrcidConfiguration.java @@ -35,6 +35,8 @@ public final class OrcidConfiguration { private String scopes; + private String revokeUrl; + public String getApiUrl() { return apiUrl; } @@ -111,4 +113,11 @@ public boolean isApiConfigured() { return !StringUtils.isAnyBlank(clientId, clientSecret); } + public String getRevokeUrl() { + return revokeUrl; + } + + public void setRevokeUrl(String revokeUrl) { + this.revokeUrl = revokeUrl; + } } diff --git a/dspace-api/src/main/java/org/dspace/orcid/consumer/OrcidQueueConsumer.java b/dspace-api/src/main/java/org/dspace/orcid/consumer/OrcidQueueConsumer.java index 97da341fb811..6b174e96957f 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/consumer/OrcidQueueConsumer.java +++ b/dspace-api/src/main/java/org/dspace/orcid/consumer/OrcidQueueConsumer.java @@ -14,9 +14,10 @@ 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 alreadyConsumedItems = new ArrayList<>(); + private final Set itemsToConsume = new HashSet<>(); @Override public void initialize() throws Exception { @@ -117,17 +118,26 @@ public void consume(Context context, Event event) throws Exception { return; } - if (alreadyConsumedItems.contains(item.getID())) { - return; - } + itemsToConsume.add(item.getID()); + } + + @Override + public void end(Context context) throws Exception { + + for (UUID itemId : itemsToConsume) { + + Item item = itemService.find(context, itemId); + + context.turnOffAuthorisationSystem(); + try { + consumeItem(context, item); + } finally { + context.restoreAuthSystemState(); + } - context.turnOffAuthorisationSystem(); - try { - consumeItem(context, item); - } finally { - context.restoreAuthSystemState(); } + itemsToConsume.clear(); } /** @@ -146,7 +156,7 @@ private void consumeItem(Context context, Item item) throws SQLException { consumeProfile(context, item); } - alreadyConsumedItems.add(item.getID()); + itemsToConsume.add(item.getID()); } @@ -169,6 +179,10 @@ private void consumeEntity(Context context, Item entity) throws SQLException { continue; } + if (isNotLatestVersion(context, entity)) { + continue; + } + orcidQueueService.create(context, relatedItem, entity); } @@ -329,6 +343,14 @@ private boolean isNotProfileItem(Item profileItemItem) { 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 @@ private boolean isOrcidSynchronizationDisabled() { 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 diff --git a/dspace-api/src/main/java/org/dspace/orcid/dao/OrcidQueueDAO.java b/dspace-api/src/main/java/org/dspace/orcid/dao/OrcidQueueDAO.java index 235443b15033..b7e0b1ed2a85 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/dao/OrcidQueueDAO.java +++ b/dspace-api/src/main/java/org/dspace/orcid/dao/OrcidQueueDAO.java @@ -74,6 +74,16 @@ public List findByProfileItemAndEntity(Context context, Item profile */ public List 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 findByEntity(Context context, Item item) throws SQLException; + /** * Find all the OrcidQueue records with the given entity and record type. * diff --git a/dspace-api/src/main/java/org/dspace/orcid/dao/impl/OrcidQueueDAOImpl.java b/dspace-api/src/main/java/org/dspace/orcid/dao/impl/OrcidQueueDAOImpl.java index c8e48e3f17d6..091e59750517 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/dao/impl/OrcidQueueDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/orcid/dao/impl/OrcidQueueDAOImpl.java @@ -63,6 +63,13 @@ public List findByProfileItemOrEntity(Context context, Item item) th return query.getResultList(); } + @Override + public List 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 findByEntityAndRecordType(Context context, Item entity, String type) throws SQLException { Query query = createQuery(context, "FROM OrcidQueue WHERE entity = :entity AND recordType = :type"); diff --git a/dspace-api/src/main/java/org/dspace/orcid/model/factory/OrcidFactoryUtils.java b/dspace-api/src/main/java/org/dspace/orcid/model/factory/OrcidFactoryUtils.java index 4b8c1178efeb..ce68ab47c26e 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/model/factory/OrcidFactoryUtils.java +++ b/dspace-api/src/main/java/org/dspace/orcid/model/factory/OrcidFactoryUtils.java @@ -7,10 +7,21 @@ */ package org.dspace.orcid.model.factory; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.dspace.app.client.DSpaceHttpClientFactory; +import org.json.JSONObject; /** * Utility class for Orcid factory classes. This is used to parse the @@ -65,4 +76,48 @@ private static String[] parseConfiguration(String configuration) { return configurations; } + /** + * Retrieve access token from ORCID, given a client ID, client secret and OAuth URL + * + * @param clientId ORCID client ID + * @param clientSecret ORCID client secret + * @param oauthUrl ORCID oauth redirect URL + * @return response object as Optional string + * @throws IOException if any errors are encountered making the connection or reading a response + */ + public static Optional retrieveAccessToken(String clientId, String clientSecret, String oauthUrl) + throws IOException { + if (StringUtils.isNotBlank(clientSecret) && StringUtils.isNotBlank(clientId) + && StringUtils.isNotBlank(oauthUrl)) { + String authenticationParameters = "?client_id=" + clientId + + "&client_secret=" + clientSecret + + "&scope=/read-public&grant_type=client_credentials"; + HttpPost httpPost = new HttpPost(oauthUrl + authenticationParameters); + httpPost.addHeader("Accept", "application/json"); + httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); + + HttpResponse response; + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { + response = httpClient.execute(httpPost); + } + JSONObject responseObject = null; + if (response != null && response.getStatusLine().getStatusCode() == 200) { + try (InputStream is = response.getEntity().getContent(); + BufferedReader streamReader = new BufferedReader(new InputStreamReader(is, + StandardCharsets.UTF_8))) { + String inputStr; + while ((inputStr = streamReader.readLine()) != null && responseObject == null) { + if (inputStr.startsWith("{") && inputStr.endsWith("}") && inputStr.contains("access_token")) { + responseObject = new JSONObject(inputStr); + } + } + } + } + if (responseObject != null && responseObject.has("access_token")) { + return Optional.of((String) responseObject.get("access_token")); + } + } + // Return empty by default + return Optional.empty(); + } } diff --git a/dspace-api/src/main/java/org/dspace/orcid/service/OrcidQueueService.java b/dspace-api/src/main/java/org/dspace/orcid/service/OrcidQueueService.java index 8de25e9caf1e..b667088eabb4 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/service/OrcidQueueService.java +++ b/dspace-api/src/main/java/org/dspace/orcid/service/OrcidQueueService.java @@ -164,6 +164,16 @@ public List findByProfileItemAndEntity(Context context, Item profile */ public List 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 findByEntity(Context context, Item item) throws SQLException; + /** * Get all the OrcidQueue records with attempts less than the given attempts. * diff --git a/dspace-api/src/main/java/org/dspace/orcid/service/impl/OrcidQueueServiceImpl.java b/dspace-api/src/main/java/org/dspace/orcid/service/impl/OrcidQueueServiceImpl.java index d3300fea6606..261f8ef9a9f7 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/service/impl/OrcidQueueServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/orcid/service/impl/OrcidQueueServiceImpl.java @@ -70,6 +70,11 @@ public List findByProfileItemOrEntity(Context context, Item item) th return orcidQueueDAO.findByProfileItemOrEntity(context, item); } + @Override + public List 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); diff --git a/dspace-api/src/main/java/org/dspace/orcid/service/impl/OrcidSynchronizationServiceImpl.java b/dspace-api/src/main/java/org/dspace/orcid/service/impl/OrcidSynchronizationServiceImpl.java index 59e4dea64145..f976864d07f9 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/service/impl/OrcidSynchronizationServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/orcid/service/impl/OrcidSynchronizationServiceImpl.java @@ -37,6 +37,7 @@ import org.dspace.eperson.EPerson; import org.dspace.eperson.service.EPersonService; 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.OrcidSynchronizationService; @@ -47,6 +48,8 @@ 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; /** @@ -57,6 +60,7 @@ */ public class OrcidSynchronizationServiceImpl implements OrcidSynchronizationService { + private static final Logger log = LoggerFactory.getLogger(OrcidSynchronizationServiceImpl.class); @Autowired private ItemService itemService; @@ -75,6 +79,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 { @@ -114,20 +121,33 @@ public void linkProfile(Context context, Item profile, OrcidTokenResponseDTO tok @Override public void unlinkProfile(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); + clearOrcidProfileMetadata(context, profile); - if (!configurationService.getBooleanProperty("orcid.disconnection.remain-sync", false)) { - clearSynchronizationSettings(context, profile); - } + clearSynchronizationSettings(context, profile); - orcidTokenService.deleteByProfileItem(context, profile); + clearOrcidToken(context, profile); updateItem(context, profile); } + 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 { @@ -273,6 +293,11 @@ private boolean updatePreferenceForSynchronizingWithOrcid(Context context, Item 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); diff --git a/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventActionServiceImpl.java b/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventActionServiceImpl.java index 30875a5105b0..6b9aea91de76 100644 --- a/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventActionServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventActionServiceImpl.java @@ -22,9 +22,9 @@ import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.content.Item; import org.dspace.content.QAEvent; import org.dspace.content.service.ItemService; @@ -114,20 +114,19 @@ public void reject(Context context, QAEvent qaevent) { * Make acknowledgement to the configured urls for the event status. */ private void makeAcknowledgement(String eventId, String source, String status) { - String[] ackwnoledgeCallbacks = configurationService + String[] acknowledgeCallbacks = configurationService .getArrayProperty("qaevents." + source + ".acknowledge-url"); - if (ackwnoledgeCallbacks != null) { - for (String ackwnoledgeCallback : ackwnoledgeCallbacks) { - if (StringUtils.isNotBlank(ackwnoledgeCallback)) { + if (acknowledgeCallbacks != null) { + for (String acknowledgeCallback : acknowledgeCallbacks) { + if (StringUtils.isNotBlank(acknowledgeCallback)) { ObjectNode node = jsonMapper.createObjectNode(); node.put("eventId", eventId); node.put("status", status); StringEntity requestEntity = new StringEntity(node.toString(), ContentType.APPLICATION_JSON); - CloseableHttpClient httpclient = HttpClients.createDefault(); - HttpPost postMethod = new HttpPost(ackwnoledgeCallback); - postMethod.setEntity(requestEntity); - try { - httpclient.execute(postMethod); + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().buildWithoutProxy()) { + HttpPost postMethod = new HttpPost(acknowledgeCallback); + postMethod.setEntity(requestEntity); + httpClient.execute(postMethod); } catch (IOException e) { log.error(e.getMessage(), e); } diff --git a/dspace-api/src/main/java/org/dspace/scripts/ProcessServiceImpl.java b/dspace-api/src/main/java/org/dspace/scripts/ProcessServiceImpl.java index f242ec7acdad..b3893f744380 100644 --- a/dspace-api/src/main/java/org/dspace/scripts/ProcessServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/scripts/ProcessServiceImpl.java @@ -45,8 +45,8 @@ 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 int countSearch(Context context, ProcessQueryParameterContainer processQu @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 void appendLog(int processId, String scriptName, String output, ProcessLo @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 int countByUser(Context context, EPerson user) throws SQLException { return processDAO.countByUser(context, user); } + @Override + public void failRunningProcesses(Context context) throws SQLException, IOException, AuthorizeException { + List 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 @@ private String formatLogLine(int processId, String scriptName, String output, Pr 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; + } } diff --git a/dspace-api/src/main/java/org/dspace/scripts/service/ProcessService.java b/dspace-api/src/main/java/org/dspace/scripts/service/ProcessService.java index c6fc24888155..5df2ca15aad8 100644 --- a/dspace-api/src/main/java/org/dspace/scripts/service/ProcessService.java +++ b/dspace-api/src/main/java/org/dspace/scripts/service/ProcessService.java @@ -277,4 +277,14 @@ List findByStatusAndCreationTimeOlderThan(Context context, List= 0 && endYearIdx < 4) { - padding = 4 - endYearIdx; - } else if (value.length() < 4) { - padding = 4 - value.length(); - } + Date result = MultiFormatDateParser.parse(value); - if (padding > 0) { - // padding the value from left with 0 so that 87 -> 0087, 687-11-24 - // -> 0687-11-24 - return String.format("%1$0" + padding + "d", 0) - + value; + // If parsing was successful we return the value as an ISO instant, + // otherwise we return null so Solr does not index this date value. + if (result != null) { + return result.toInstant().toString(); } else { - return value; + return null; } } } diff --git a/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java b/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java index 68a73f7d7a67..32de86744d13 100644 --- a/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java @@ -51,7 +51,6 @@ import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.solr.client.solrj.SolrClient; @@ -81,6 +80,7 @@ import org.apache.solr.common.params.ModifiableSolrParams; import org.apache.solr.common.params.ShardParams; import org.apache.solr.common.util.NamedList; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.Bitstream; import org.dspace.content.Bundle; @@ -1045,7 +1045,6 @@ public QueryResponse query(String query, String filterQuery, String facetField, return null; } - // System.out.println("QUERY"); SolrQuery solrQuery = new SolrQuery().setRows(rows).setQuery(query) .setFacetMinCount(facetMinCount); addAdditionalSolrYearCores(solrQuery); @@ -1223,7 +1222,7 @@ public void shardSolrIndex() throws IOException, SolrServerException { + "." + i + ".csv"); - try ( CloseableHttpClient hc = HttpClientBuilder.create().build(); ) { + try (CloseableHttpClient hc = DSpaceHttpClientFactory.getInstance().buildWithoutProxy()) { HttpResponse response = hc.execute(get); csvInputstream = response.getEntity().getContent(); //Write the csv ouput to a file ! @@ -1365,7 +1364,7 @@ public void reindexBitstreamHits(boolean removeDeletedBitstreams) throws Excepti HttpGet get = new HttpGet(solrRequestUrl); List rows; - try ( CloseableHttpClient hc = HttpClientBuilder.create().build(); ) { + try (CloseableHttpClient hc = DSpaceHttpClientFactory.getInstance().buildWithoutProxy()) { HttpResponse response = hc.execute(get); InputStream csvOutput = response.getEntity().getContent(); Reader csvReader = new InputStreamReader(csvOutput); diff --git a/dspace-api/src/main/java/org/dspace/statistics/export/service/OpenUrlServiceImpl.java b/dspace-api/src/main/java/org/dspace/statistics/export/service/OpenUrlServiceImpl.java index b7a9562fb541..64014fcbc07c 100644 --- a/dspace-api/src/main/java/org/dspace/statistics/export/service/OpenUrlServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/statistics/export/service/OpenUrlServiceImpl.java @@ -14,13 +14,13 @@ import java.util.List; import org.apache.commons.lang.StringUtils; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.core.Context; import org.dspace.statistics.export.OpenURLTracker; import org.springframework.beans.factory.annotation.Autowired; @@ -68,16 +68,16 @@ public void processUrl(Context c, String urlStr) throws SQLException { * @throws IOException */ protected int getResponseCodeFromUrl(final String urlStr) throws IOException { - HttpGet httpGet = new HttpGet(urlStr); - HttpClient httpClient = getHttpClient(getHttpClientRequestConfig()); - HttpResponse httpResponse = httpClient.execute(httpGet); - return httpResponse.getStatusLine().getStatusCode(); + try (CloseableHttpClient httpClient = getHttpClient(getHttpClientRequestConfig())) { + HttpGet httpGet = new HttpGet(urlStr); + try (CloseableHttpResponse httpResponse = httpClient.execute(httpGet)) { + return httpResponse.getStatusLine().getStatusCode(); + } + } } - protected HttpClient getHttpClient(RequestConfig requestConfig) { - return HttpClientBuilder.create() - .setDefaultRequestConfig(requestConfig) - .build(); + protected CloseableHttpClient getHttpClient(RequestConfig requestConfig) { + return DSpaceHttpClientFactory.getInstance().buildWithRequestConfig(requestConfig); } protected RequestConfig getHttpClientRequestConfig() { diff --git a/dspace-api/src/main/java/org/dspace/statistics/util/StatisticsImporter.java b/dspace-api/src/main/java/org/dspace/statistics/util/StatisticsImporter.java index 95736a8bd6d9..354c803fe2ae 100644 --- a/dspace-api/src/main/java/org/dspace/statistics/util/StatisticsImporter.java +++ b/dspace-api/src/main/java/org/dspace/statistics/util/StatisticsImporter.java @@ -357,7 +357,7 @@ protected void load(String filename, Context context, boolean verbose) { 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 static void main(String[] args) throws Exception { 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 static void main(String[] args) throws Exception { "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); } diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java index 6fef7365e482..7743b93ca4ba 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java @@ -12,6 +12,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Path; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -249,7 +250,15 @@ protected File getFile(Bitstream bitstream) throws IOException { log.debug("Local filename for " + sInternalId + " is " + bufFilename.toString()); } - return new File(bufFilename.toString()); + File bitstreamFile = new File(bufFilename.toString()); + Path normalizedPath = bitstreamFile.toPath().normalize(); + if (!normalizedPath.startsWith(baseDir.getAbsolutePath())) { + log.error("Bitstream path outside of assetstore root requested:" + + "bitstream={}, path={}, assetstore={}", + bitstream.getID(), normalizedPath, baseDir.getAbsolutePath()); + throw new IOException("Illegal bitstream path constructed"); + } + return bitstreamFile; } public boolean isRegisteredBitstream(String internalId) { diff --git a/dspace-api/src/main/java/org/dspace/usage/package-info.java b/dspace-api/src/main/java/org/dspace/usage/package-info.java index 5883bcf358f4..26984ae0caa0 100644 --- a/dspace-api/src/main/java/org/dspace/usage/package-info.java +++ b/dspace-api/src/main/java/org/dspace/usage/package-info.java @@ -25,7 +25,7 @@ * {@code EventService}, as with the stock listeners. *

* - * @see org.dspace.google.GoogleRecorderEventListener + * @see org.dspace.google.GoogleAsyncEventListener * @see org.dspace.statistics.SolrLoggerUsageEventListener */ diff --git a/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java b/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java index 63b5391d0a28..27a81a157917 100644 --- a/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java +++ b/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java @@ -33,6 +33,11 @@ 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 void initialize() throws Exception { 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 void consume(Context ctx, Event event) throws Exception { // 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 @@ protected void unarchiveItem(Context ctx, Item item) { )); } + 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 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 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. diff --git a/dspace-api/src/main/java/org/dspace/vocabulary/ControlledVocabulary.java b/dspace-api/src/main/java/org/dspace/vocabulary/ControlledVocabulary.java index 7f2bdc6ef771..bd19a1254fe3 100644 --- a/dspace-api/src/main/java/org/dspace/vocabulary/ControlledVocabulary.java +++ b/dspace-api/src/main/java/org/dspace/vocabulary/ControlledVocabulary.java @@ -12,7 +12,6 @@ import java.util.ArrayList; import java.util.List; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.xpath.XPath; @@ -20,6 +19,7 @@ import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; +import org.dspace.app.util.XMLUtils; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; import org.w3c.dom.Document; @@ -71,7 +71,7 @@ public static ControlledVocabulary loadVocabulary(String fileName) File controlledVocFile = new File(filePath.toString()); if (controlledVocFile.exists()) { - DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + DocumentBuilder builder = XMLUtils.getDocumentBuilder(); Document document = builder.parse(controlledVocFile); XPath xPath = XPathFactory.newInstance().newXPath(); Node node = (Node) xPath.compile("node").evaluate(document, XPathConstants.NODE); diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ScoreReviewAction.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ScoreReviewAction.java index a8ed4fd3dae9..50f338499282 100644 --- a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ScoreReviewAction.java +++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ScoreReviewAction.java @@ -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.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 @@ 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 @@ private boolean checkRequestValid(int score, String review) { @Override public List getOptions() { - return List.of(SUBMIT_SCORE, RETURN_TO_POOL); + List 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 diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/SingleUserReviewAction.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/SingleUserReviewAction.java index 64e0957b65b7..c46fa851e4f1 100644 --- a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/SingleUserReviewAction.java +++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/SingleUserReviewAction.java @@ -21,6 +21,8 @@ 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 @@ 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 ActionResult processAccept(Context c, XmlWorkflowItem wfi) throws SQLExce public List getOptions() { List 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; diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/storedcomponents/PoolTaskServiceImpl.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/storedcomponents/PoolTaskServiceImpl.java index fb673725e181..d3c8f6334d8f 100644 --- a/dspace-api/src/main/java/org/dspace/xmlworkflow/storedcomponents/PoolTaskServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/storedcomponents/PoolTaskServiceImpl.java @@ -13,6 +13,7 @@ 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 PoolTask findByWorkflowIdAndEPerson(Context context, XmlWorkflowItem work //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 groups = groupService.allMemberGroupsSet(context, ePerson); - for (Group group : groups) { - poolTask = poolTaskDAO.findByWorkflowItemAndGroup(context, group, workflowItem); - if (poolTask != null) { - return poolTask; - } + List generalTasks = poolTaskDAO.findByWorkflowItem(context, workflowItem); + Optional firstClaimedTask = groups.stream() + .flatMap(group -> generalTasks.stream() + .filter(f -> f.getGroup().getID().equals(group.getID())) + .findFirst() + .stream()) + .findFirst(); + + if (firstClaimedTask.isPresent()) { + return firstClaimedTask.get(); } } } diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2024.12.17__workspaceitem_add_item_id_unique_constraint.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2024.12.17__workspaceitem_add_item_id_unique_constraint.sql new file mode 100644 index 000000000000..38389bf2d19b --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2024.12.17__workspaceitem_add_item_id_unique_constraint.sql @@ -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); diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2024.12.17__workspaceitem_add_item_id_unique_constraint.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2024.12.17__workspaceitem_add_item_id_unique_constraint.sql new file mode 100644 index 000000000000..20eb0f9119d3 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2024.12.17__workspaceitem_add_item_id_unique_constraint.sql @@ -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); diff --git a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml index f7943fb2320c..e693d26e538e 100644 --- a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml +++ b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml @@ -59,7 +59,7 @@ - + diff --git a/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml b/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml index feb3c7c12b3d..51c6a228d4fb 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml @@ -29,6 +29,7 @@ + @@ -194,7 +195,11 @@ org.dspace.app.rest.submit.step.NotifyStep coarnotify - + + submit.progressbar.upload-no-required-metadata + org.dspace.app.rest.submit.step.UploadStep + upload + @@ -300,6 +305,12 @@ + + + + + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/local.cfg b/dspace-api/src/test/data/dspaceFolder/config/local.cfg index b44f319a35f6..962a1bcd9b22 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/local.cfg +++ b/dspace-api/src/test/data/dspaceFolder/config/local.cfg @@ -156,10 +156,9 @@ 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 +management.health.seo.enabled = false # Enable researcher profiles and orcid synchronization for tests researcher-profile.entity-type = Person @@ -191,4 +190,4 @@ ldn.notify.inbox.block-untrusted-ip = true # ERROR LOGGING # ########################################### # Log full stacktrace of other common 4xx errors (for easier debugging of these errors in tests) -logging.server.include-stacktrace-for-httpcode = 422, 400 \ No newline at end of file +logging.server.include-stacktrace-for-httpcode = 422, 400 diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/access-conditions.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/access-conditions.xml index a9af7c66f5e8..bf02e6a23ed2 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/access-conditions.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/access-conditions.xml @@ -55,6 +55,7 @@ + @@ -116,4 +117,16 @@
+ + + + + + + + + + + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/orcid-authority-services.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/orcid-authority-services.xml index 4a73b215cd4b..3e38055b678a 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/orcid-authority-services.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/orcid-authority-services.xml @@ -16,7 +16,7 @@ - + diff --git a/dspace-api/src/test/java/org/dspace/AbstractDSpaceIntegrationTest.java b/dspace-api/src/test/java/org/dspace/AbstractDSpaceIntegrationTest.java index 5a5ce8bf6d4c..791fdbc66abc 100644 --- a/dspace-api/src/test/java/org/dspace/AbstractDSpaceIntegrationTest.java +++ b/dspace-api/src/test/java/org/dspace/AbstractDSpaceIntegrationTest.java @@ -21,8 +21,12 @@ 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 static void initTestEnvironment() { } } + @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. diff --git a/dspace-api/src/test/java/org/dspace/AbstractDSpaceTest.java b/dspace-api/src/test/java/org/dspace/AbstractDSpaceTest.java index 36477556d3de..136af83f076f 100644 --- a/dspace-api/src/test/java/org/dspace/AbstractDSpaceTest.java +++ b/dspace-api/src/test/java/org/dspace/AbstractDSpaceTest.java @@ -18,9 +18,13 @@ 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 @@ protected 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 static void initKernel() { } } + @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 diff --git a/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java b/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java index 9bacbb97eec4..76b3fe131be0 100644 --- a/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java +++ b/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java @@ -20,8 +20,8 @@ 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 void setUp() throws Exception { 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 void setUp() throws Exception { 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()"); diff --git a/dspace-api/src/test/java/org/dspace/administer/StructBuilderIT.java b/dspace-api/src/test/java/org/dspace/administer/StructBuilderIT.java index 63340698ac00..ead338bc8e70 100644 --- a/dspace-api/src/test/java/org/dspace/administer/StructBuilderIT.java +++ b/dspace-api/src/test/java/org/dspace/administer/StructBuilderIT.java @@ -23,11 +23,12 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.dspace.AbstractIntegrationTest; +import org.dspace.AbstractIntegrationTestWithDatabase; import org.dspace.authorize.AuthorizeException; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; import org.dspace.content.Collection; import org.dspace.content.Community; -import org.dspace.content.MetadataSchemaEnum; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.CollectionService; import org.dspace.content.service.CommunityService; @@ -38,7 +39,6 @@ import org.junit.Test; import org.w3c.dom.Attr; import org.w3c.dom.Node; -import org.xml.sax.SAXException; import org.xmlunit.builder.DiffBuilder; import org.xmlunit.diff.Comparison; import org.xmlunit.diff.ComparisonFormatter; @@ -52,7 +52,7 @@ * @author Mark H. Wood */ public class StructBuilderIT - extends AbstractIntegrationTest { + extends AbstractIntegrationTestWithDatabase { private static final Logger log = LogManager.getLogger(); private static final CommunityService communityService @@ -79,7 +79,8 @@ public static void tearDownClass() { * @throws IOException passed through. */ @Before - public void setUp() throws SQLException, AuthorizeException, IOException { + public void setUp() throws Exception { + super.setUp(); // Clear out all communities and collections. context.turnOffAuthorisationSystem(); for (Community community : communityService.findAllTop(context)) { @@ -285,19 +286,15 @@ public void testImportStructureWithHandles() * @throws org.dspace.authorize.AuthorizeException passed through. */ @Test - public void testExportStructure() - throws ParserConfigurationException, SAXException, IOException, - SQLException, AuthorizeException { + public void testExportStructure() { // Create some structure to test. context.turnOffAuthorisationSystem(); - Community community0 = communityService.create(null, context); - communityService.setMetadataSingleValue(context, community0, - MetadataSchemaEnum.DC.getName(), "title", null, - null, "Top Community 0"); - Collection collection0_0 = collectionService.create(context, community0); - collectionService.setMetadataSingleValue(context, collection0_0, - MetadataSchemaEnum.DC.getName(), "title", null, - null, "Collection 0.0"); + // Top level community + Community community0 = CommunityBuilder.createCommunity(context) + .withName("Top Community 0").build(); + // Collection below top level community + Collection collection0_0 = CollectionBuilder.createCollection(context, community0) + .withName("Collection 0.0").build(); // Export the current structure. System.out.println("exportStructure"); diff --git a/dspace-api/src/test/java/org/dspace/app/bulkaccesscontrol/BulkAccessControlIT.java b/dspace-api/src/test/java/org/dspace/app/bulkaccesscontrol/BulkAccessControlIT.java index 73f02e40494c..106b176ee5b4 100644 --- a/dspace-api/src/test/java/org/dspace/app/bulkaccesscontrol/BulkAccessControlIT.java +++ b/dspace-api/src/test/java/org/dspace/app/bulkaccesscontrol/BulkAccessControlIT.java @@ -32,8 +32,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -1831,6 +1833,88 @@ public void performBulkAccessWithHelpParamTest() throws Exception { assertThat(testDSpaceRunnableHandler.getWarningMessages(), empty()); } + + @Test + public void bulkAccessControlShouldProcessEachItemOnceWithPagination() throws Exception { + context.turnOffAuthorisationSystem(); + + Community community = CommunityBuilder.createCommunity(context) + .withName("Test Community") + .build(); + + Collection collection = CollectionBuilder.createCollection(context, community) + .withName("Test Collection") + .build(); + + List createdItemIDs = new ArrayList<>(); + + for (int i = 0; i < 25; i++) { + Item item = ItemBuilder.createItem(context, collection).build(); + + Bundle bundle = BundleBuilder.createBundle(context, item) + .withName("ORIGINAL") + .build(); + + BitstreamBuilder.createBitstream(context, bundle, + IOUtils.toInputStream("Bitstream content " + i, + CharEncoding.UTF_8)) + .withName("bitstream_" + i) + .build(); + + createdItemIDs.add(item.getID()); + } + + context.restoreAuthSystemState(); + + // JSON without constraints: apply to ALL items + String json = """ + { "item": { + "mode": "add", + "accessConditions": [ + { + "name": "openaccess" + } + ] + }} + """; + + buildJsonFile(json); + + String[] args = { + "bulk-access-control", + "-u", community.getID().toString(), + "-f", tempFilePath, + "-e", admin.getEmail() + }; + + TestDSpaceRunnableHandler testHandler = new TestDSpaceRunnableHandler(); + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), testHandler, kernelImpl); + + assertThat(testHandler.getErrorMessages(), empty()); + assertThat(testHandler.getWarningMessages(), empty()); + + // Collect item IDs from the info messages + List infoMessages = testHandler.getInfoMessages(); + List updatedItemIDs = infoMessages.stream() + .map(msg -> { + int startIdx = msg.indexOf("Item {") + 6; + int endIdx = msg.indexOf("}", startIdx); + return UUID.fromString(msg.substring(startIdx, endIdx)); + }) + .toList(); + + Set uniqueUpdatedItemIDs = new HashSet<>(updatedItemIDs); + + // Check if any item was processed multiple times + assertThat("Some items were processed more than once!", + uniqueUpdatedItemIDs.size(), is(updatedItemIDs.size())); + + // Check all items were updated once + assertThat("Not all created items were updated!", + createdItemIDs, containsInAnyOrder(uniqueUpdatedItemIDs.toArray())); + } + + private List findItems(String query) throws SearchServiceException { DiscoverQuery discoverQuery = new DiscoverQuery(); diff --git a/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataExportSearchIT.java b/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataExportSearchIT.java index 3a972692efeb..63a87a48f554 100644 --- a/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataExportSearchIT.java +++ b/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataExportSearchIT.java @@ -23,7 +23,8 @@ 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; diff --git a/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataImportIT.java b/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataImportIT.java index ae079df560ed..de1dcc91c9a1 100644 --- a/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataImportIT.java +++ b/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataImportIT.java @@ -75,6 +75,31 @@ public void setUp() throws Exception { context.restoreAuthSystemState(); } + @Test + public void metadataImportTestWithDuplicateHeader() { + String[] csv = {"id,collection,dc.title,dc.title,dc.contributor.author", + "+," + collection.getHandle() + ",\"Test Import 1\",\"Test Import 2\"," + "\"Donald, SmithImported\"," + + "+," + collection.getHandle() + ",\"Test Import 3\",\"Test Import 4\"," + "\"Donald, SmithImported\""}; + // Should throw an exception because of duplicate header + try { + performImportScript(csv); + } catch (Exception e) { + assertTrue(e instanceof MetadataImportInvalidHeadingException); + } + } + + @Test + public void metadataImportTestWithAnyLanguage() { + String[] csv = {"id,collection,dc.title[*],dc.contributor.author", + "+," + collection.getHandle() + ",\"Test Import 1\"," + "\"Donald, SmithImported\""}; + // Should throw an exception because of invalid ANY language (*) in metadata field + try { + performImportScript(csv); + } catch (Exception e) { + assertTrue(e instanceof MetadataImportInvalidHeadingException); + } + } + @Test public void metadataImportTest() throws Exception { String[] csv = {"id,collection,dc.title,dc.contributor.author", @@ -230,7 +255,7 @@ public void metadataImportRemovingValueTest() throws Exception { itemService.getMetadata(item, "dc", "contributor", "author", Item.ANY).get(0).getValue(), "TestAuthorToRemove")); - String[] csv = {"id,collection,dc.title,dc.contributor.author[*]", + String[] csv = {"id,collection,dc.title,dc.contributor.author", item.getID().toString() + "," + personCollection.getHandle() + "," + item.getName() + ","}; performImportScript(csv); item = findItemByName(itemTitle); diff --git a/dspace-api/src/test/java/org/dspace/app/client/DSpaceHttpClientFactoryTest.java b/dspace-api/src/test/java/org/dspace/app/client/DSpaceHttpClientFactoryTest.java new file mode 100644 index 000000000000..b518f19ff4d3 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/client/DSpaceHttpClientFactoryTest.java @@ -0,0 +1,256 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.client; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.arrayWithSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.HttpResponse; +import org.apache.http.HttpResponseInterceptor; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.conn.routing.HttpRoute; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.protocol.HttpContext; +import org.dspace.services.ConfigurationService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** + * Unit tests for {@link DSpaceHttpClientFactory}. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +@RunWith(MockitoJUnitRunner.class) +public class DSpaceHttpClientFactoryTest { + + @InjectMocks + private DSpaceHttpClientFactory httpClientFactory; + + @Mock + private ConfigurationService configurationService; + + private MockWebServer mockProxy; + + private MockWebServer mockServer; + + @Before + public void init() { + this.httpClientFactory.setProxyRoutePlanner(new DSpaceProxyRoutePlanner(configurationService)); + this.mockProxy = new MockWebServer(); + this.mockProxy.enqueue(new MockResponse().setResponseCode(200).addHeader("From", "Proxy")); + this.mockServer = new MockWebServer(); + this.mockServer.enqueue(new MockResponse().setResponseCode(200).addHeader("From", "Server")); + } + + @Test + public void testBuildWithProxyConfigured() throws Exception { + setHttpProxyOnConfigurationService(); + CloseableHttpClient httpClient = httpClientFactory.build(); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(0)); + CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(mockServer.url("").toString())); + assertThat(httpResponse.getHeaders("From"), arrayWithSize(1)); + assertThat(httpResponse.getHeaders("From")[0].getValue(), is("Proxy")); + assertThat(mockProxy.getRequestCount(), is(1)); + assertThat(mockServer.getRequestCount(), is(0)); + RecordedRequest request = mockProxy.takeRequest(100, TimeUnit.MILLISECONDS); + assertThat(request, notNullValue()); + assertThat(request.getRequestUrl(), is(mockProxy.url(""))); + assertThat(request.getRequestLine(), is("GET " + mockServer.url("").toString() + " HTTP/1.1")); + verify(configurationService).getProperty("http.proxy.host"); + verify(configurationService).getProperty("http.proxy.port"); + verify(configurationService).getArrayProperty("http.proxy.hosts-to-ignore"); + verifyNoMoreInteractions(configurationService); + } + + @Test + public void testBuildWithProxyConfiguredAndHostToIgnoreSet() throws Exception { + setHttpProxyOnConfigurationService(mockServer.getHostName()); + CloseableHttpClient httpClient = httpClientFactory.build(); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(0)); + CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(mockServer.url("").toString())); + assertThat(httpResponse.getHeaders("From"), arrayWithSize(1)); + assertThat(httpResponse.getHeaders("From")[0].getValue(), is("Server")); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(1)); + verify(configurationService).getArrayProperty("http.proxy.hosts-to-ignore"); + verifyNoMoreInteractions(configurationService); + } + + @Test + public void testBuildWithProxyConfiguredAndHostPrefixToIgnoreSet() throws Exception { + setHttpProxyOnConfigurationService("local*", "www.test.com"); + CloseableHttpClient httpClient = httpClientFactory.build(); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(0)); + CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(mockServer.url("").toString())); + assertThat(httpResponse.getHeaders("From"), arrayWithSize(1)); + assertThat(httpResponse.getHeaders("From")[0].getValue(), is("Server")); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(1)); + verify(configurationService).getArrayProperty("http.proxy.hosts-to-ignore"); + verifyNoMoreInteractions(configurationService); + } + + @Test + public void testBuildWithProxyConfiguredAndHostSuffixToIgnoreSet() throws Exception { + setHttpProxyOnConfigurationService("www.test.com", "*host"); + CloseableHttpClient httpClient = httpClientFactory.build(); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(0)); + CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(mockServer.url("").toString())); + assertThat(httpResponse.getHeaders("From"), arrayWithSize(1)); + assertThat(httpResponse.getHeaders("From")[0].getValue(), is("Server")); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(1)); + verify(configurationService).getArrayProperty("http.proxy.hosts-to-ignore"); + verifyNoMoreInteractions(configurationService); + } + + @Test + public void testBuildWithoutConfiguredProxy() throws Exception { + CloseableHttpClient httpClient = httpClientFactory.build(); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(0)); + CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(mockServer.url("").toString())); + assertThat(httpResponse.getHeaders("From"), arrayWithSize(1)); + assertThat(httpResponse.getHeaders("From")[0].getValue(), is("Server")); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(1)); + RecordedRequest request = mockServer.takeRequest(100, TimeUnit.MILLISECONDS); + assertThat(request, notNullValue()); + assertThat(request.getRequestUrl(), is(mockServer.url(""))); + assertThat(request.getRequestLine(), is("GET / HTTP/1.1")); + verify(configurationService).getProperty("http.proxy.host"); + verify(configurationService).getProperty("http.proxy.port"); + verify(configurationService).getArrayProperty("http.proxy.hosts-to-ignore"); + verifyNoMoreInteractions(configurationService); + } + + @Test + public void testBuildWithoutProxy() throws Exception { + CloseableHttpClient httpClient = httpClientFactory.buildWithoutProxy(); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(0)); + httpClient.execute(new HttpGet(mockServer.url("").toString())); + assertThat(mockServer.getRequestCount(), is(1)); + assertThat(mockProxy.getRequestCount(), is(0)); + RecordedRequest request = mockServer.takeRequest(100, TimeUnit.MILLISECONDS); + assertThat(request, notNullValue()); + assertThat(request.getRequestUrl(), is(mockServer.url(""))); + assertThat(request.getRequestLine(), is("GET / HTTP/1.1")); + verifyNoInteractions(configurationService); + } + + @Test + public void testBuildWithoutAutomaticRetries() throws Exception { + setHttpProxyOnConfigurationService("www.test.com"); + CloseableHttpClient httpClient = httpClientFactory.buildWithoutAutomaticRetries(10); + httpClient.execute(new HttpGet(mockServer.url("").toString())); + assertThat(mockProxy.getRequestCount(), is(1)); + assertThat(mockServer.getRequestCount(), is(0)); + RecordedRequest request = mockProxy.takeRequest(100, TimeUnit.MILLISECONDS); + assertThat(request, notNullValue()); + assertThat(request.getRequestUrl(), is(mockProxy.url(""))); + assertThat(request.getRequestLine(), is("GET " + mockServer.url("").toString() + " HTTP/1.1")); + verify(configurationService).getProperty("http.proxy.host"); + verify(configurationService).getProperty("http.proxy.port"); + verify(configurationService).getArrayProperty("http.proxy.hosts-to-ignore"); + verifyNoMoreInteractions(configurationService); + } + + @Test + public void testBuildWithHttpRequestInterceptor() throws Exception { + setHttpProxyOnConfigurationService("*test.com", "www.dspace.com"); + AtomicReference contextReference = new AtomicReference(); + HttpRequestInterceptor interceptor = (request, context) -> contextReference.set(context); + httpClientFactory.setRequestInterceptors(List.of(interceptor)); + CloseableHttpClient httpClient = httpClientFactory.build(); + httpClient.execute(new HttpGet(mockServer.url("").toString())); + assertThat(mockProxy.getRequestCount(), is(1)); + assertThat(mockServer.getRequestCount(), is(0)); + HttpContext httpContext = contextReference.get(); + assertThat(httpContext, notNullValue()); + Object httpRouteObj = httpContext.getAttribute("http.route"); + assertThat(httpRouteObj, notNullValue()); + assertThat(httpRouteObj, instanceOf(HttpRoute.class)); + HttpRoute httpRoute = (HttpRoute) httpRouteObj; + assertThat(httpRoute.getHopCount(), is(2)); + assertThat(httpRoute.getHopTarget(0).getPort(), is(mockProxy.getPort())); + assertThat(httpRoute.getHopTarget(1).getPort(), is(mockServer.getPort())); + } + + @Test + public void testBuildWithHttpResponseInterceptor() throws Exception { + AtomicReference responseReference = new AtomicReference(); + HttpResponseInterceptor responseInterceptor = (response, context) -> responseReference.set(response); + httpClientFactory.setResponseInterceptors(List.of(responseInterceptor)); + CloseableHttpClient httpClient = httpClientFactory.build(); + httpClient.execute(new HttpGet(mockServer.url("").toString())); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(1)); + HttpResponse httpResponse = responseReference.get(); + assertThat(httpResponse, notNullValue()); + assertThat(httpResponse.getHeaders("From"), arrayWithSize(1)); + assertThat(httpResponse.getHeaders("From")[0].getValue(), is("Server")); + } + + @Test + public void testBuildWithRequestConfig() throws Exception { + setHttpProxyOnConfigurationService(); + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(2500) + .build(); + AtomicReference contextReference = new AtomicReference(); + HttpRequestInterceptor interceptor = (request, context) -> contextReference.set(context); + httpClientFactory.setRequestInterceptors(List.of(interceptor)); + CloseableHttpClient httpClient = httpClientFactory.buildWithRequestConfig(requestConfig); + httpClient.execute(new HttpGet(mockServer.url("").toString())); + assertThat(mockProxy.getRequestCount(), is(1)); + assertThat(mockServer.getRequestCount(), is(0)); + HttpContext httpContext = contextReference.get(); + assertThat(httpContext, notNullValue()); + Object httpRequestConfigObj = httpContext.getAttribute("http.request-config"); + assertThat(httpRequestConfigObj, notNullValue()); + assertThat(httpRequestConfigObj, instanceOf(RequestConfig.class)); + assertThat(((RequestConfig) httpRequestConfigObj).getConnectTimeout(), is(2500)); + verify(configurationService).getProperty("http.proxy.host"); + verify(configurationService).getProperty("http.proxy.port"); + verify(configurationService).getArrayProperty("http.proxy.hosts-to-ignore"); + verifyNoMoreInteractions(configurationService); + } + + private void setHttpProxyOnConfigurationService(String... hostsToIgnore) { + when(configurationService.getProperty("http.proxy.host")).thenReturn(mockProxy.getHostName()); + when(configurationService.getProperty("http.proxy.port")).thenReturn(String.valueOf(mockProxy.getPort())); + when(configurationService.getArrayProperty("http.proxy.hosts-to-ignore")).thenReturn(hostsToIgnore); + } +} \ No newline at end of file diff --git a/dspace-api/src/test/java/org/dspace/app/packager/PackagerIT.java b/dspace-api/src/test/java/org/dspace/app/packager/PackagerIT.java index 2cddbb511f91..aeda97f818c2 100644 --- a/dspace-api/src/test/java/org/dspace/app/packager/PackagerIT.java +++ b/dspace-api/src/test/java/org/dspace/app/packager/PackagerIT.java @@ -24,6 +24,7 @@ import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; import org.dspace.builder.ItemBuilder; +import org.dspace.builder.WorkspaceItemBuilder; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.Item; @@ -159,7 +160,7 @@ public void packagerUUIDAlreadyExistWithoutForceTest() throws Exception { performExportScript(article.getHandle(), tempFile); UUID id = article.getID(); itemService.delete(context, article); - WorkspaceItem workspaceItem = workspaceItemService.create(context, col1, id, false, false); + WorkspaceItem workspaceItem = WorkspaceItemBuilder.createWorkspaceItem(context, col1, id).build(); installItemService.installItem(context, workspaceItem, "123456789/0100"); performImportNoForceScript(tempFile); Iterator items = itemService.findByCollection(context, col1); diff --git a/dspace-api/src/test/java/org/dspace/app/sherpa/SHERPADataProviderTest.java b/dspace-api/src/test/java/org/dspace/app/sherpa/SHERPADataProviderTest.java index cbea55ea0787..f37d9469d12d 100644 --- a/dspace-api/src/test/java/org/dspace/app/sherpa/SHERPADataProviderTest.java +++ b/dspace-api/src/test/java/org/dspace/app/sherpa/SHERPADataProviderTest.java @@ -38,6 +38,12 @@ public class SHERPADataProviderTest extends AbstractDSpaceTest { ExternalDataProvider sherpaPublisherProvider; ExternalDataProvider sherpaJournalIssnProvider; + private static final MetadataFieldRef TITLE_FIELD = new MetadataFieldRef("dc", "title", null); + private static final MetadataFieldRef ISSN_FIELD = new MetadataFieldRef("creativeworkseries", "issn", null); + private static final MetadataFieldRef SHERPA_PUBLISHER_FIELD = + new MetadataFieldRef("dc", "identifier", "sherpaPublisher"); + private static final MetadataFieldRef OTHER_FIELD = new MetadataFieldRef("dc", "identifier", "other"); + @BeforeClass public static void setUpClass() { } @@ -84,12 +90,9 @@ public void testGetJournalISSNExternalObject() { String title = null; String identifier = null; for (MetadataValueDTO metadataValue : dataObject.getMetadata()) { - if (metadataValue.getSchema().equalsIgnoreCase("dc") && - metadataValue.getElement().equalsIgnoreCase("title")) { + if (TITLE_FIELD.matches(metadataValue)) { title = metadataValue.getValue(); - } else if (metadataValue.getSchema().equalsIgnoreCase("dc") - && metadataValue.getElement().equalsIgnoreCase("identifier") - && metadataValue.getQualifier().equalsIgnoreCase("issn")) { + } else if (ISSN_FIELD.matches(metadataValue)) { identifier = metadataValue.getValue(); } } @@ -133,12 +136,9 @@ public void testSearchJournalISSNExternalObjects() { String title = null; String identifier = null; for (MetadataValueDTO metadataValue : dataObject.getMetadata()) { - if (metadataValue.getSchema().equalsIgnoreCase("dc") && - metadataValue.getElement().equalsIgnoreCase("title")) { + if (TITLE_FIELD.matches(metadataValue)) { title = metadataValue.getValue(); - } else if (metadataValue.getSchema().equalsIgnoreCase("dc") - && metadataValue.getElement().equalsIgnoreCase("identifier") - && metadataValue.getQualifier().equalsIgnoreCase("issn")) { + } else if (ISSN_FIELD.matches(metadataValue)) { identifier = metadataValue.getValue(); } } @@ -174,12 +174,9 @@ public void testGetJournalExternalObject() { String title = null; String identifier = null; for (MetadataValueDTO metadataValue : dataObject.getMetadata()) { - if (metadataValue.getSchema().equalsIgnoreCase("dc") && - metadataValue.getElement().equalsIgnoreCase("title")) { + if (TITLE_FIELD.matches(metadataValue)) { title = metadataValue.getValue(); - } else if (metadataValue.getSchema().equalsIgnoreCase("dc") - && metadataValue.getElement().equalsIgnoreCase("identifier") - && metadataValue.getQualifier().equalsIgnoreCase("issn")) { + } else if (ISSN_FIELD.matches(metadataValue)) { identifier = metadataValue.getValue(); } } @@ -224,12 +221,9 @@ public void testSearchJournalObjects() { String title = null; String identifier = null; for (MetadataValueDTO metadataValue : dataObject.getMetadata()) { - if (metadataValue.getSchema().equalsIgnoreCase("dc") && - metadataValue.getElement().equalsIgnoreCase("title")) { + if (TITLE_FIELD.matches(metadataValue)) { title = metadataValue.getValue(); - } else if (metadataValue.getSchema().equalsIgnoreCase("dc") - && metadataValue.getElement().equalsIgnoreCase("identifier") - && metadataValue.getQualifier().equalsIgnoreCase("issn")) { + } else if (ISSN_FIELD.matches(metadataValue)) { identifier = metadataValue.getValue(); } } @@ -270,16 +264,11 @@ public void testGetPublisherExternalObject() { String identifier = null; String url = null; for (MetadataValueDTO metadataValue : dataObject.getMetadata()) { - if (metadataValue.getSchema().equalsIgnoreCase("dc") && - metadataValue.getElement().equalsIgnoreCase("title")) { + if (TITLE_FIELD.matches(metadataValue)) { title = metadataValue.getValue(); - } else if (metadataValue.getSchema().equalsIgnoreCase("dc") - && metadataValue.getElement().equalsIgnoreCase("identifier") - && metadataValue.getQualifier().equalsIgnoreCase("sherpaPublisher")) { + } else if (SHERPA_PUBLISHER_FIELD.matches(metadataValue)) { identifier = metadataValue.getValue(); - } else if (metadataValue.getSchema().equalsIgnoreCase("dc") - && metadataValue.getElement().equalsIgnoreCase("identifier") - && metadataValue.getQualifier().equalsIgnoreCase("other")) { + } else if (OTHER_FIELD.matches(metadataValue)) { url = metadataValue.getValue(); } } @@ -330,16 +319,11 @@ public void testSearchPublisherExternalObjects() { String identifier = null; String url = null; for (MetadataValueDTO metadataValue : dataObject.getMetadata()) { - if (metadataValue.getSchema().equalsIgnoreCase("dc") && - metadataValue.getElement().equalsIgnoreCase("title")) { + if (TITLE_FIELD.matches(metadataValue)) { title = metadataValue.getValue(); - } else if (metadataValue.getSchema().equalsIgnoreCase("dc") - && metadataValue.getElement().equalsIgnoreCase("identifier") - && metadataValue.getQualifier().equalsIgnoreCase("sherpaPublisher")) { + } else if (SHERPA_PUBLISHER_FIELD.matches(metadataValue)) { identifier = metadataValue.getValue(); - } else if (metadataValue.getSchema().equalsIgnoreCase("dc") - && metadataValue.getElement().equalsIgnoreCase("identifier") - && metadataValue.getQualifier().equalsIgnoreCase("other")) { + } else if (OTHER_FIELD.matches(metadataValue)) { url = metadataValue.getValue(); } } @@ -380,12 +364,9 @@ public void testComparePublisherExternalObjects() { exemplarDataObject.setId(validIdentifier); exemplarDataObject.setValue(validName); exemplarDataObject.setDisplayValue(validName); - exemplarDataObject.addMetadata(new MetadataValueDTO("dc", "title", null, null, - validName)); - exemplarDataObject.addMetadata(new MetadataValueDTO("dc", "identifier", "sherpaPublisher", null, - validIdentifier)); - exemplarDataObject.addMetadata(new MetadataValueDTO("dc", "identifier", "other", null, - validUrl)); + exemplarDataObject.addMetadata(TITLE_FIELD.toMetadata(validName)); + exemplarDataObject.addMetadata(SHERPA_PUBLISHER_FIELD.toMetadata(validIdentifier)); + exemplarDataObject.addMetadata(OTHER_FIELD.toMetadata(validUrl)); // Exemplar object 2 has a different order of metadata values // (we still expect it to be 'equal' when comparing since there is no concept of place for DTOs) @@ -394,12 +375,9 @@ public void testComparePublisherExternalObjects() { exemplarDataObject2.setId(validIdentifier); exemplarDataObject2.setValue(validName); exemplarDataObject2.setDisplayValue(validName); - exemplarDataObject2.addMetadata(new MetadataValueDTO("dc", "identifier", "other", null, - validUrl)); - exemplarDataObject2.addMetadata(new MetadataValueDTO("dc", "title", null, null, - validName)); - exemplarDataObject2.addMetadata(new MetadataValueDTO("dc", "identifier", "sherpaPublisher", null, - validIdentifier)); + exemplarDataObject2.addMetadata(OTHER_FIELD.toMetadata(validUrl)); + exemplarDataObject2.addMetadata(TITLE_FIELD.toMetadata(validName)); + exemplarDataObject2.addMetadata(SHERPA_PUBLISHER_FIELD.toMetadata(validIdentifier)); // Nonequal object should NOT evaluate as equal to our data ExternalDataObject nonEqualObject = new ExternalDataObject(); @@ -407,12 +385,9 @@ public void testComparePublisherExternalObjects() { nonEqualObject.setId(validIdentifier); nonEqualObject.setValue(validName); nonEqualObject.setDisplayValue(validName); - nonEqualObject.addMetadata(new MetadataValueDTO("dc", "title", null, null, - "Private Library of Science")); - nonEqualObject.addMetadata(new MetadataValueDTO("dc", "identifier", "sherpaPublisher", null, - validIdentifier)); - nonEqualObject.addMetadata(new MetadataValueDTO("dc", "identifier", "other", null, - validUrl)); + nonEqualObject.addMetadata(TITLE_FIELD.toMetadata("Private Library of Science")); + nonEqualObject.addMetadata(SHERPA_PUBLISHER_FIELD.toMetadata(validIdentifier)); + nonEqualObject.addMetadata(OTHER_FIELD.toMetadata(validUrl)); // Retrieve the dataobject(s) from the data provider @@ -437,4 +412,28 @@ public void testComparePublisherExternalObjects() { // Assert NON-equality to the 3rd object assertNotEquals(nonEqualObject, dataObject); } + + private static class MetadataFieldRef { + public final String schema; + public final String element; + public final String qualifier; + + public MetadataFieldRef(String schema, String element, String qualifier) { + this.schema = schema; + this.element = element; + this.qualifier = qualifier; + } + + public boolean matches(MetadataValueDTO value) { + return schema.equalsIgnoreCase(value.getSchema()) && + element.equalsIgnoreCase(value.getElement()) && + (qualifier == null ? value.getQualifier() == null + : qualifier.equalsIgnoreCase(value.getQualifier())); + } + + public MetadataValueDTO toMetadata(String value) { + return new MetadataValueDTO(schema, element, qualifier, null, value); + } + } + } \ No newline at end of file diff --git a/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java b/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java index 562aa86a585e..511df79f1e50 100644 --- a/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java +++ b/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java @@ -26,25 +26,47 @@ */ public class MockOrcid extends Orcidv3SolrAuthorityImpl { + OrcidRestConnector orcidRestConnector; + @Override public void init() { - OrcidRestConnector orcidRestConnector = Mockito.mock(OrcidRestConnector.class); + initializeAccessToken(); + orcidRestConnector = Mockito.mock(OrcidRestConnector.class); + } + + /** + * Call this to set up mocking for any test classes that need it. We don't set it in init() + * or other AbstractIntegrationTest implementations will complain of unnecessary Mockito stubbing + */ + public void setupNoResultsSearch() { when(orcidRestConnector.get(ArgumentMatchers.startsWith("search?"), ArgumentMatchers.any())) - .thenAnswer(new Answer() { - @Override - public InputStream answer(InvocationOnMock invocation) { - return this.getClass().getResourceAsStream("orcid-search-noresults.xml"); - } - }); + .thenAnswer(new Answer() { + @Override + public InputStream answer(InvocationOnMock invocation) { + return this.getClass().getResourceAsStream("orcid-search-noresults.xml"); + } + }); + } + /** + * Call this to set up mocking for any test classes that need it. We don't set it in init() + * or other AbstractIntegrationTest implementations will complain of unnecessary Mockito stubbing + */ + public void setupSingleSearch() { when(orcidRestConnector.get(ArgumentMatchers.startsWith("search?q=Bollini"), ArgumentMatchers.any())) - .thenAnswer(new Answer() { + .thenAnswer(new Answer() { @Override public InputStream answer(InvocationOnMock invocation) { return this.getClass().getResourceAsStream("orcid-search.xml"); } }); + } + /** + * Call this to set up mocking for any test classes that need it. We don't set it in init() + * or other AbstractIntegrationTest implementations will complain of unnecessary Mockito stubbing + */ + public void setupSearchWithResults() { when(orcidRestConnector.get(ArgumentMatchers.endsWith("/person"), ArgumentMatchers.any())) - .thenAnswer(new Answer() { + .thenAnswer(new Answer() { @Override public InputStream answer(InvocationOnMock invocation) { return this.getClass().getResourceAsStream("orcid-person-record.xml"); @@ -54,4 +76,10 @@ public InputStream answer(InvocationOnMock invocation) { setOrcidRestConnector(orcidRestConnector); } + @Override + public void initializeAccessToken() { + if (getAccessToken() == null) { + setAccessToken("mock-access-token"); + } + } } diff --git a/dspace-api/src/test/java/org/dspace/builder/AbstractDSpaceObjectBuilder.java b/dspace-api/src/test/java/org/dspace/builder/AbstractDSpaceObjectBuilder.java index e7ebd8768e7d..fa7306ad9955 100644 --- a/dspace-api/src/test/java/org/dspace/builder/AbstractDSpaceObjectBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/AbstractDSpaceObjectBuilder.java @@ -18,7 +18,6 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.ResourcePolicy; import org.dspace.content.DSpaceObject; -import org.dspace.content.Item; import org.dspace.content.service.DSpaceObjectService; import org.dspace.core.Constants; import org.dspace.core.Context; @@ -103,7 +102,7 @@ protected > B setMetadataSingleValue(fi final String qualifier, final String value) { try { - getService().setMetadataSingleValue(context, dso, schema, element, qualifier, Item.ANY, value); + getService().setMetadataSingleValue(context, dso, schema, element, qualifier, null, value); } catch (Exception e) { return handleException(e); } diff --git a/dspace-api/src/test/java/org/dspace/builder/GroupBuilder.java b/dspace-api/src/test/java/org/dspace/builder/GroupBuilder.java index b3447dd8bd9a..c16fb696b0c3 100644 --- a/dspace-api/src/test/java/org/dspace/builder/GroupBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/GroupBuilder.java @@ -12,6 +12,9 @@ import java.util.UUID; import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.DSpaceObject; import org.dspace.content.service.DSpaceObjectService; import org.dspace.core.Context; import org.dspace.eperson.EPerson; @@ -51,6 +54,33 @@ public static GroupBuilder createGroup(final Context context) { return builder.create(context); } + public static GroupBuilder createCollectionAdminGroup(final Context context, Collection collection) { + GroupBuilder builder = new GroupBuilder(context); + return builder.createAdminGroup(context, collection); + } + + public static GroupBuilder createCollectionSubmitterGroup(final Context context, Collection collection) { + GroupBuilder builder = new GroupBuilder(context); + return builder.createSubmitterGroup(context, collection); + } + + public static GroupBuilder createCollectionDefaultReadGroup(final Context context, Collection collection, + String typeOfGroupString, int defaultRead) { + GroupBuilder builder = new GroupBuilder(context); + return builder.createDefaultReadGroup(context, collection, typeOfGroupString, defaultRead); + } + + public static GroupBuilder createCollectionWorkflowRoleGroup(final Context context, Collection collection, + String roleName) { + GroupBuilder builder = new GroupBuilder(context); + return builder.createWorkflowRoleGroup(context, collection, roleName); + } + + public static GroupBuilder createCommunityAdminGroup(final Context context, Community community) { + GroupBuilder builder = new GroupBuilder(context); + return builder.createAdminGroup(context, community); + } + private GroupBuilder create(final Context context) { this.context = context; try { @@ -61,6 +91,54 @@ private GroupBuilder create(final Context context) { return this; } + private GroupBuilder createAdminGroup(final Context context, DSpaceObject container) { + this.context = context; + try { + if (container instanceof Collection) { + group = collectionService.createAdministrators(context, (Collection) container); + } else if (container instanceof Community) { + group = communityService.createAdministrators(context, (Community) container); + } else { + handleException(new IllegalArgumentException("DSpaceObject must be collection or community. " + + "Type: " + container.getType())); + } + } catch (Exception e) { + return handleException(e); + } + return this; + } + + private GroupBuilder createSubmitterGroup(final Context context, Collection collection) { + this.context = context; + try { + group = collectionService.createSubmitters(context, collection); + } catch (Exception e) { + return handleException(e); + } + return this; + } + + private GroupBuilder createDefaultReadGroup(final Context context, Collection collection, + String typeOfGroupString, int defaultRead) { + this.context = context; + try { + group = collectionService.createDefaultReadGroup(context, collection, typeOfGroupString, defaultRead); + } catch (Exception e) { + return handleException(e); + } + return this; + } + + private GroupBuilder createWorkflowRoleGroup(final Context context, Collection collection, String roleName) { + this.context = context; + try { + group = workflowService.createWorkflowRoleGroup(context, collection, roleName); + } catch (Exception e) { + return handleException(e); + } + return this; + } + @Override protected DSpaceObjectService getService() { return groupService; diff --git a/dspace-api/src/test/java/org/dspace/builder/OrcidHistoryBuilder.java b/dspace-api/src/test/java/org/dspace/builder/OrcidHistoryBuilder.java index 199f412f8506..d811d03f5358 100644 --- a/dspace-api/src/test/java/org/dspace/builder/OrcidHistoryBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/OrcidHistoryBuilder.java @@ -11,7 +11,8 @@ 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 @@ */ public class OrcidHistoryBuilder extends AbstractBuilder { - private static final Logger log = Logger.getLogger(OrcidHistoryBuilder.class); + private static final Logger log = LogManager.getLogger(OrcidHistoryBuilder.class); private OrcidHistory orcidHistory; diff --git a/dspace-api/src/test/java/org/dspace/builder/WorkspaceItemBuilder.java b/dspace-api/src/test/java/org/dspace/builder/WorkspaceItemBuilder.java index 8b82149cdf7f..67d8894338eb 100644 --- a/dspace-api/src/test/java/org/dspace/builder/WorkspaceItemBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/WorkspaceItemBuilder.java @@ -10,6 +10,7 @@ import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; +import java.util.UUID; import org.dspace.app.ldn.NotifyPatternToTrigger; import org.dspace.app.ldn.NotifyServiceEntity; @@ -43,14 +44,31 @@ protected WorkspaceItemBuilder(Context context) { public static WorkspaceItemBuilder createWorkspaceItem(final Context context, final Collection col) { WorkspaceItemBuilder builder = new WorkspaceItemBuilder(context); - return builder.create(context, col); + return builder.create(context, col, null); } - private WorkspaceItemBuilder create(final Context context, final Collection col) { + public static WorkspaceItemBuilder createWorkspaceItem(final Context context, final Collection col, UUID uuid) { + WorkspaceItemBuilder builder = new WorkspaceItemBuilder(context); + return builder.create(context, col, uuid); + } + + /** + * Create with a specific UUID (e.g. restoring items with Packager import) + * + * @param context DSpace context + * @param col Parent collection + * @param uuid Item UUID + * @return WorkspaceItemBuilder + */ + private WorkspaceItemBuilder create(final Context context, final Collection col, UUID uuid) { this.context = context; try { - workspaceItem = workspaceItemService.create(context, col, false); + if (uuid == null) { + workspaceItem = workspaceItemService.create(context, col, false); + } else { + workspaceItem = workspaceItemService.create(context, col, uuid, false, false); + } item = workspaceItem.getItem(); } catch (Exception e) { return handleException(e); diff --git a/dspace-api/src/test/java/org/dspace/content/ItemComparatorTest.java b/dspace-api/src/test/java/org/dspace/content/ItemComparatorTest.java index 54ff9ce02624..be670d9b5097 100644 --- a/dspace-api/src/test/java/org/dspace/content/ItemComparatorTest.java +++ b/dspace-api/src/test/java/org/dspace/content/ItemComparatorTest.java @@ -141,37 +141,37 @@ public void testCompare() throws SQLException { assertTrue("testCompare 0", result == 0); ic = new ItemComparator("test", "one", Item.ANY, true); - itemService.addMetadata(context, one, "dc", "test", "one", Item.ANY, "1"); + itemService.addMetadata(context, one, "dc", "test", "one", null, "1"); result = ic.compare(one, two); assertTrue("testCompare 1", result >= 1); itemService.clearMetadata(context, one, "dc", "test", "one", Item.ANY); ic = new ItemComparator("test", "one", Item.ANY, true); - itemService.addMetadata(context, two, "dc", "test", "one", Item.ANY, "1"); + itemService.addMetadata(context, two, "dc", "test", "one", null, "1"); result = ic.compare(one, two); assertTrue("testCompare 2", result <= -1); itemService.clearMetadata(context, two, "dc", "test", "one", Item.ANY); //value in both items ic = new ItemComparator("test", "one", Item.ANY, true); - itemService.addMetadata(context, one, "dc", "test", "one", Item.ANY, "1"); - itemService.addMetadata(context, two, "dc", "test", "one", Item.ANY, "2"); + itemService.addMetadata(context, one, "dc", "test", "one", null, "1"); + itemService.addMetadata(context, two, "dc", "test", "one", null, "2"); result = ic.compare(one, two); assertTrue("testCompare 3", result <= -1); itemService.clearMetadata(context, one, "dc", "test", "one", Item.ANY); itemService.clearMetadata(context, two, "dc", "test", "one", Item.ANY); ic = new ItemComparator("test", "one", Item.ANY, true); - itemService.addMetadata(context, one, "dc", "test", "one", Item.ANY, "1"); - itemService.addMetadata(context, two, "dc", "test", "one", Item.ANY, "1"); + itemService.addMetadata(context, one, "dc", "test", "one", null, "1"); + itemService.addMetadata(context, two, "dc", "test", "one", null, "1"); result = ic.compare(one, two); assertTrue("testCompare 4", result == 0); itemService.clearMetadata(context, one, "dc", "test", "one", Item.ANY); itemService.clearMetadata(context, two, "dc", "test", "one", Item.ANY); ic = new ItemComparator("test", "one", Item.ANY, true); - itemService.addMetadata(context, one, "dc", "test", "one", Item.ANY, "2"); - itemService.addMetadata(context, two, "dc", "test", "one", Item.ANY, "1"); + itemService.addMetadata(context, one, "dc", "test", "one", null, "2"); + itemService.addMetadata(context, two, "dc", "test", "one", null, "1"); result = ic.compare(one, two); assertTrue("testCompare 5", result >= 1); itemService.clearMetadata(context, one, "dc", "test", "one", Item.ANY); @@ -179,60 +179,60 @@ public void testCompare() throws SQLException { //multiple values (min, max) ic = new ItemComparator("test", "one", Item.ANY, true); - itemService.addMetadata(context, one, "dc", "test", "one", Item.ANY, "0"); - itemService.addMetadata(context, one, "dc", "test", "one", Item.ANY, "1"); - itemService.addMetadata(context, two, "dc", "test", "one", Item.ANY, "2"); - itemService.addMetadata(context, two, "dc", "test", "one", Item.ANY, "3"); + itemService.addMetadata(context, one, "dc", "test", "one", null, "0"); + itemService.addMetadata(context, one, "dc", "test", "one", null, "1"); + itemService.addMetadata(context, two, "dc", "test", "one", null, "2"); + itemService.addMetadata(context, two, "dc", "test", "one", null, "3"); result = ic.compare(one, two); assertTrue("testCompare 3", result <= -1); itemService.clearMetadata(context, one, "dc", "test", "one", Item.ANY); itemService.clearMetadata(context, two, "dc", "test", "one", Item.ANY); ic = new ItemComparator("test", "one", Item.ANY, true); - itemService.addMetadata(context, one, "dc", "test", "one", Item.ANY, "0"); - itemService.addMetadata(context, one, "dc", "test", "one", Item.ANY, "1"); - itemService.addMetadata(context, two, "dc", "test", "one", Item.ANY, "-1"); - itemService.addMetadata(context, two, "dc", "test", "one", Item.ANY, "1"); + itemService.addMetadata(context, one, "dc", "test", "one", null, "0"); + itemService.addMetadata(context, one, "dc", "test", "one", null, "1"); + itemService.addMetadata(context, two, "dc", "test", "one", null, "-1"); + itemService.addMetadata(context, two, "dc", "test", "one", null, "1"); result = ic.compare(one, two); assertTrue("testCompare 4", result == 0); itemService.clearMetadata(context, one, "dc", "test", "one", Item.ANY); itemService.clearMetadata(context, two, "dc", "test", "one", Item.ANY); ic = new ItemComparator("test", "one", Item.ANY, true); - itemService.addMetadata(context, one, "dc", "test", "one", Item.ANY, "1"); - itemService.addMetadata(context, one, "dc", "test", "one", Item.ANY, "2"); - itemService.addMetadata(context, two, "dc", "test", "one", Item.ANY, "1"); - itemService.addMetadata(context, two, "dc", "test", "one", Item.ANY, "-1"); + itemService.addMetadata(context, one, "dc", "test", "one", null, "1"); + itemService.addMetadata(context, one, "dc", "test", "one", null, "2"); + itemService.addMetadata(context, two, "dc", "test", "one", null, "1"); + itemService.addMetadata(context, two, "dc", "test", "one", null, "-1"); result = ic.compare(one, two); assertTrue("testCompare 5", result >= 1); itemService.clearMetadata(context, one, "dc", "test", "one", Item.ANY); itemService.clearMetadata(context, two, "dc", "test", "one", Item.ANY); ic = new ItemComparator("test", "one", Item.ANY, false); - itemService.addMetadata(context, one, "dc", "test", "one", Item.ANY, "1"); - itemService.addMetadata(context, one, "dc", "test", "one", Item.ANY, "2"); - itemService.addMetadata(context, two, "dc", "test", "one", Item.ANY, "2"); - itemService.addMetadata(context, two, "dc", "test", "one", Item.ANY, "3"); + itemService.addMetadata(context, one, "dc", "test", "one", null, "1"); + itemService.addMetadata(context, one, "dc", "test", "one", null, "2"); + itemService.addMetadata(context, two, "dc", "test", "one", null, "2"); + itemService.addMetadata(context, two, "dc", "test", "one", null, "3"); result = ic.compare(one, two); assertTrue("testCompare 3", result <= -1); itemService.clearMetadata(context, one, "dc", "test", "one", Item.ANY); itemService.clearMetadata(context, two, "dc", "test", "one", Item.ANY); ic = new ItemComparator("test", "one", Item.ANY, false); - itemService.addMetadata(context, one, "dc", "test", "one", Item.ANY, "1"); - itemService.addMetadata(context, one, "dc", "test", "one", Item.ANY, "2"); - itemService.addMetadata(context, two, "dc", "test", "one", Item.ANY, "1"); - itemService.addMetadata(context, two, "dc", "test", "one", Item.ANY, "5"); + itemService.addMetadata(context, one, "dc", "test", "one", null, "1"); + itemService.addMetadata(context, one, "dc", "test", "one", null, "2"); + itemService.addMetadata(context, two, "dc", "test", "one", null, "1"); + itemService.addMetadata(context, two, "dc", "test", "one", null, "5"); result = ic.compare(one, two); assertTrue("testCompare 4", result == 0); itemService.clearMetadata(context, one, "dc", "test", "one", Item.ANY); itemService.clearMetadata(context, two, "dc", "test", "one", Item.ANY); ic = new ItemComparator("test", "one", Item.ANY, false); - itemService.addMetadata(context, one, "dc", "test", "one", Item.ANY, "2"); - itemService.addMetadata(context, one, "dc", "test", "one", Item.ANY, "3"); - itemService.addMetadata(context, two, "dc", "test", "one", Item.ANY, "1"); - itemService.addMetadata(context, two, "dc", "test", "one", Item.ANY, "4"); + itemService.addMetadata(context, one, "dc", "test", "one", null, "2"); + itemService.addMetadata(context, one, "dc", "test", "one", null, "3"); + itemService.addMetadata(context, two, "dc", "test", "one", null, "1"); + itemService.addMetadata(context, two, "dc", "test", "one", null, "4"); result = ic.compare(one, two); assertTrue("testCompare 5", result >= 1); itemService.clearMetadata(context, one, "dc", "test", "one", Item.ANY); diff --git a/dspace-api/src/test/java/org/dspace/content/ItemTest.java b/dspace-api/src/test/java/org/dspace/content/ItemTest.java index aaa28769dca6..00dbf2994d98 100644 --- a/dspace-api/src/test/java/org/dspace/content/ItemTest.java +++ b/dspace-api/src/test/java/org/dspace/content/ItemTest.java @@ -518,11 +518,11 @@ public void testAddMetadata_5args_1() throws SQLException { String schema = "dc"; String element = "contributor"; String qualifier = "author"; - String lang = Item.ANY; + String lang = null; String[] values = {"value0", "value1"}; itemService.addMetadata(context, it, schema, element, qualifier, lang, Arrays.asList(values)); - List dc = itemService.getMetadata(it, schema, element, qualifier, lang); + List dc = itemService.getMetadata(it, schema, element, qualifier, Item.ANY); assertThat("testAddMetadata_5args_1 0", dc, notNullValue()); assertTrue("testAddMetadata_5args_1 1", dc.size() == 2); assertThat("testAddMetadata_5args_1 2", dc.get(0).getMetadataField().getMetadataSchema().getName(), @@ -544,7 +544,7 @@ public void testAddMetadata_5args_no_values() throws Exception { String schema = "dc"; String element = "contributor"; String qualifier = "author"; - String lang = Item.ANY; + String lang = null; String[] values = {}; itemService.addMetadata(context, it, schema, element, qualifier, lang, Arrays.asList(values)); fail("IllegalArgumentException expected"); @@ -563,13 +563,13 @@ public void testAddMetadata_7args_1_authority() String schema = "dc"; String element = "language"; String qualifier = "iso"; - String lang = Item.ANY; + String lang = null; List values = Arrays.asList("en_US", "en"); List authorities = Arrays.asList("accepted", "uncertain"); List confidences = Arrays.asList(0, 0); itemService.addMetadata(context, it, schema, element, qualifier, lang, values, authorities, confidences); - List dc = itemService.getMetadata(it, schema, element, qualifier, lang); + List dc = itemService.getMetadata(it, schema, element, qualifier, Item.ANY); assertThat("testAddMetadata_7args_1 0", dc, notNullValue()); assertTrue("testAddMetadata_7args_1 1", dc.size() == 2); assertThat("testAddMetadata_7args_1 2", dc.get(0).getMetadataField().getMetadataSchema().getName(), @@ -600,13 +600,13 @@ public void testAddMetadata_7args_1_noauthority() throws SQLException { String schema = "dc"; String element = "contributor"; String qualifier = "author"; - String lang = Item.ANY; + String lang = null; List values = Arrays.asList("value0", "value1"); List authorities = Arrays.asList("auth0", "auth2"); List confidences = Arrays.asList(0, 0); itemService.addMetadata(context, it, schema, element, qualifier, lang, values, authorities, confidences); - List dc = itemService.getMetadata(it, schema, element, qualifier, lang); + List dc = itemService.getMetadata(it, schema, element, qualifier, Item.ANY); assertThat("testAddMetadata_7args_1 0", dc, notNullValue()); assertTrue("testAddMetadata_7args_1 1", dc.size() == 2); assertThat("testAddMetadata_7args_1 2", dc.get(0).getMetadataField().getMetadataSchema().getName(), @@ -632,7 +632,7 @@ public void testAddMetadata_7args_no_values() throws Exception { String schema = "dc"; String element = "contributor"; String qualifier = "author"; - String lang = Item.ANY; + String lang = null; List values = new ArrayList(); List authorities = new ArrayList(); List confidences = new ArrayList(); @@ -645,7 +645,7 @@ public void testAddMetadata_list_with_virtual_metadata() throws Exception { String schema = "dc"; String element = "contributor"; String qualifier = "author"; - String lang = Item.ANY; + String lang = null; // Create two fake virtual metadata ("virtual::[relationship-id]") values List values = new ArrayList<>(Arrays.asList("uuid-1", "uuid-2")); List authorities = new ArrayList<>(Arrays.asList(Constants.VIRTUAL_AUTHORITY_PREFIX + "relationship-1", @@ -674,7 +674,7 @@ public void testAddMetadata_list_with_virtual_metadata() throws Exception { assertEquals(1, valuesAdded.size()); // Get metadata and ensure new value is the ONLY ONE for this metadata field - List dc = itemService.getMetadata(it, schema, element, qualifier, lang); + List dc = itemService.getMetadata(it, schema, element, qualifier, Item.ANY); assertNotNull(dc); assertEquals(1, dc.size()); assertEquals(schema, dc.get(0).getMetadataField().getMetadataSchema().getName()); @@ -693,11 +693,11 @@ public void testAddMetadata_5args_2() throws SQLException { String schema = "dc"; String element = "contributor"; String qualifier = "author"; - String lang = Item.ANY; + String lang = null; String value = "value0"; itemService.addMetadata(context, it, schema, element, qualifier, lang, value); - List dc = itemService.getMetadata(it, schema, element, qualifier, lang); + List dc = itemService.getMetadata(it, schema, element, qualifier, Item.ANY); assertThat("testAddMetadata_5args_2 0", dc, notNullValue()); assertTrue("testAddMetadata_5args_2 1", dc.size() == 1); assertThat("testAddMetadata_5args_2 2", dc.get(0).getMetadataField().getMetadataSchema().getName(), @@ -719,13 +719,13 @@ public void testAddMetadata_7args_2_authority() throws SQLException { String schema = "dc"; String element = "language"; String qualifier = "iso"; - String lang = Item.ANY; + String lang = null; String values = "en"; String authorities = "accepted"; int confidences = 0; itemService.addMetadata(context, it, schema, element, qualifier, lang, values, authorities, confidences); - List dc = itemService.getMetadata(it, schema, element, qualifier, lang); + List dc = itemService.getMetadata(it, schema, element, qualifier, Item.ANY); assertThat("testAddMetadata_7args_2 0", dc, notNullValue()); assertTrue("testAddMetadata_7args_2 1", dc.size() == 1); assertThat("testAddMetadata_7args_2 2", dc.get(0).getMetadataField().getMetadataSchema().getName(), @@ -748,13 +748,13 @@ public void testAddMetadata_7args_2_noauthority() throws SQLException { String schema = "dc"; String element = "contributor"; String qualifier = "editor"; - String lang = Item.ANY; + String lang = null; String values = "value0"; String authorities = "auth0"; int confidences = 0; itemService.addMetadata(context, it, schema, element, qualifier, lang, values, authorities, confidences); - List dc = itemService.getMetadata(it, schema, element, qualifier, lang); + List dc = itemService.getMetadata(it, schema, element, qualifier, Item.ANY); assertThat("testAddMetadata_7args_2 0", dc, notNullValue()); assertTrue("testAddMetadata_7args_2 1", dc.size() == 1); assertThat("testAddMetadata_7args_2 2", dc.get(0).getMetadataField().getMetadataSchema().getName(), @@ -772,7 +772,7 @@ public void testAddMetadata_single_virtual_metadata() throws Exception { String schema = "dc"; String element = "contributor"; String qualifier = "author"; - String lang = Item.ANY; + String lang = null; // Create a single fake virtual metadata ("virtual::[relationship-id]") value String value = "uuid-1"; String authority = Constants.VIRTUAL_AUTHORITY_PREFIX + "relationship-1"; @@ -786,7 +786,7 @@ public void testAddMetadata_single_virtual_metadata() throws Exception { assertNull(valuesAdded); // Verify this metadata field does NOT exist on the item - List mv = itemService.getMetadata(it, schema, element, qualifier, lang); + List mv = itemService.getMetadata(it, schema, element, qualifier, Item.ANY); assertNotNull(mv); assertTrue(mv.isEmpty()); @@ -797,7 +797,7 @@ public void testAddMetadata_single_virtual_metadata() throws Exception { assertNull(valuesAdded); // Verify this metadata field does NOT exist on the item - mv = itemService.getMetadata(it, schema, element, qualifier, lang); + mv = itemService.getMetadata(it, schema, element, qualifier, Item.ANY); assertNotNull(mv); assertTrue(mv.isEmpty()); } @@ -811,13 +811,13 @@ public void testClearMetadata() throws SQLException { String schema = "dc"; String element = "contributor"; String qualifier = "author"; - String lang = Item.ANY; + String lang = null; String values = "value0"; itemService.addMetadata(context, it, schema, element, qualifier, lang, values); - itemService.clearMetadata(context, it, schema, element, qualifier, lang); + itemService.clearMetadata(context, it, schema, element, qualifier, Item.ANY); - List dc = itemService.getMetadata(it, schema, element, qualifier, lang); + List dc = itemService.getMetadata(it, schema, element, qualifier, Item.ANY); assertThat("testClearMetadata 0", dc, notNullValue()); assertTrue("testClearMetadata 1", dc.size() == 0); } @@ -859,11 +859,11 @@ public void testGetCollections() throws Exception { context.turnOffAuthorisationSystem(); Collection collection = collectionService.create(context, owningCommunity); collectionService.setMetadataSingleValue(context, collection, MetadataSchemaEnum.DC.getName(), - "title", null, Item.ANY, "collection B"); + "title", null, null, "collection B"); it.addCollection(collection); collection = collectionService.create(context, owningCommunity); collectionService.setMetadataSingleValue(context, collection, MetadataSchemaEnum.DC.getName(), - "title", null, Item.ANY, "collection A"); + "title", null, null, "collection A"); it.addCollection(collection); context.restoreAuthSystemState(); assertThat("testGetCollections 0", it.getCollections(), notNullValue()); @@ -1772,7 +1772,7 @@ public void testFindByMetadataField() throws Exception { // add new metadata to item context.turnOffAuthorisationSystem(); - itemService.addMetadata(context, it, schema, element, qualifier, Item.ANY, value); + itemService.addMetadata(context, it, schema, element, qualifier, null, value); itemService.update(context, it); context.restoreAuthSystemState(); @@ -1837,7 +1837,7 @@ public void testFindByAuthorityValue() throws Exception { // add new metadata (with authority) to item context.turnOffAuthorisationSystem(); - itemService.addMetadata(context, it, schema, element, qualifier, Item.ANY, value, authority, confidence); + itemService.addMetadata(context, it, schema, element, qualifier, null, value, authority, confidence); itemService.update(context, it); context.restoreAuthSystemState(); diff --git a/dspace-api/src/test/java/org/dspace/content/MetadataDSpaceCsvExportServiceImplIT.java b/dspace-api/src/test/java/org/dspace/content/MetadataDSpaceCsvExportServiceImplIT.java index c2d4f56ca61a..5a3412646482 100644 --- a/dspace-api/src/test/java/org/dspace/content/MetadataDSpaceCsvExportServiceImplIT.java +++ b/dspace-api/src/test/java/org/dspace/content/MetadataDSpaceCsvExportServiceImplIT.java @@ -16,6 +16,7 @@ import org.dspace.AbstractIntegrationTestWithDatabase; import org.dspace.app.bulkedit.DSpaceCSV; import org.dspace.app.bulkedit.DSpaceCSVLine; +import org.dspace.app.scripts.handler.impl.TestDSpaceRunnableHandler; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; import org.dspace.builder.ItemBuilder; @@ -31,6 +32,9 @@ */ public class MetadataDSpaceCsvExportServiceImplIT extends AbstractIntegrationTestWithDatabase { + + TestDSpaceRunnableHandler testDSpaceRunnableHandler = new TestDSpaceRunnableHandler(); + /** * Test of handleExport method, of class MetadataDSpaceCsvExportServiceImpl. * @throws java.lang.Exception passed through. @@ -66,7 +70,7 @@ public void testExport_3args_1() boolean exportAll = false; MetadataDSpaceCsvExportServiceImpl instance = new MetadataDSpaceCsvExportServiceImpl(); DSpaceCSV expResult = null; - DSpaceCSV result = instance.export(context, toExport, exportAll); + DSpaceCSV result = instance.export(context, toExport, exportAll, testDSpaceRunnableHandler); assertEquals(expResult, result); // TODO review the generated test code and remove the default call to fail. fail("The test case is a prototype."); @@ -105,7 +109,7 @@ public void testMappedItem() .getServiceManager() .getServiceByName(MetadataDSpaceCsvExportServiceImpl.class.getCanonicalName(), MetadataDSpaceCsvExportService.class); - DSpaceCSV result = instance.export(context, parentCommunity, false); + DSpaceCSV result = instance.export(context, parentCommunity, false, testDSpaceRunnableHandler); // Examine the result. List csvLines = result.getCSVLines(); diff --git a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsIT.java b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsIT.java index 44653300e0de..9ff452f7895f 100644 --- a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsIT.java +++ b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsIT.java @@ -49,6 +49,7 @@ import org.dspace.builder.ItemBuilder; import org.dspace.builder.RelationshipBuilder; import org.dspace.builder.RelationshipTypeBuilder; +import org.dspace.builder.VersionBuilder; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.InstallItemService; import org.dspace.content.service.ItemService; @@ -58,12 +59,10 @@ 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; -import org.dspace.versioning.factory.VersionServiceFactory; -import org.dspace.versioning.service.VersioningService; import org.hamcrest.Matcher; import org.junit.Assert; import org.junit.Before; @@ -74,16 +73,14 @@ public class VersioningWithRelationshipsIT extends AbstractIntegrationTestWithDa private final RelationshipService relationshipService = ContentServiceFactory.getInstance().getRelationshipService(); - private final VersioningService versioningService = - VersionServiceFactory.getInstance().getVersionService(); private final WorkspaceItemService workspaceItemService = ContentServiceFactory.getInstance().getWorkspaceItemService(); private final InstallItemService installItemService = 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; @@ -291,7 +288,7 @@ public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Except // create a new version of the publication // ///////////////////////////////////////////// - Version newVersion = versioningService.createNewVersion(context, originalPublication); + Version newVersion = VersionBuilder.createVersion(context, originalPublication, "test").build(); Item newPublication = newVersion.getItem(); assertNotSame(originalPublication, newPublication); @@ -567,7 +564,7 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception // create a new version of the publication // ///////////////////////////////////////////// - Version newVersion = versioningService.createNewVersion(context, originalPublication); + Version newVersion = VersionBuilder.createVersion(context, originalPublication, "test").build(); Item newPublication = newVersion.getItem(); assertNotSame(originalPublication, newPublication); @@ -927,7 +924,7 @@ public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Excep // create a new version of the person // //////////////////////////////////////// - Version newVersion = versioningService.createNewVersion(context, originalPerson); + Version newVersion = VersionBuilder.createVersion(context, originalPerson, "test").build(); Item newPerson = newVersion.getItem(); assertNotSame(originalPerson, newPerson); @@ -1300,7 +1297,7 @@ public void test_createNewVersionOfItemAndVerifyMetadataOrder() throws Exception // create new version of publication // /////////////////////////////////////// - Version newVersion = versioningService.createNewVersion(context, originalPublication); + Version newVersion = VersionBuilder.createVersion(context, originalPublication, "test").build(); Item newPublication = newVersion.getItem(); assertNotSame(originalPublication, newPublication); @@ -1463,7 +1460,7 @@ public void test_createNewVersionOfItemWithAddRemoveMove() throws Exception { // create a new version of the publication // ///////////////////////////////////////////// - Version newVersion = versioningService.createNewVersion(context, originalPublication); + Version newVersion = VersionBuilder.createVersion(context, originalPublication, "test").build(); Item newPublication = newVersion.getItem(); assertNotSame(originalPublication, newPublication); @@ -1782,7 +1779,7 @@ public void test_placeRecalculationAfterDelete() throws Exception { // create new version - volume 1.2 // ///////////////////////////////////// - Item v1_2 = versioningService.createNewVersion(context, v1_1).getItem(); + Item v1_2 = VersionBuilder.createVersion(context, v1_1, "test").build().getItem(); installItemService.installItem(context, workspaceItemService.findByItem(context, v1_2)); context.commit(); @@ -1790,7 +1787,7 @@ public void test_placeRecalculationAfterDelete() throws Exception { // create new version - issue 3.2 // //////////////////////////////////// - Item i3_2 = versioningService.createNewVersion(context, i3_1).getItem(); + Item i3_2 = VersionBuilder.createVersion(context, i3_1, "test").build().getItem(); installItemService.installItem(context, workspaceItemService.findByItem(context, i3_2)); context.commit(); @@ -2316,7 +2313,7 @@ public void test_placeRecalculationAfterDelete_complex() throws Exception { // create new version - person 3.2 // ///////////////////////////////////// - Item pe3_2 = versioningService.createNewVersion(context, pe3_1).getItem(); + Item pe3_2 = VersionBuilder.createVersion(context, pe3_1, "test").build().getItem(); installItemService.installItem(context, workspaceItemService.findByItem(context, pe3_2)); context.commit(); @@ -2324,7 +2321,7 @@ public void test_placeRecalculationAfterDelete_complex() throws Exception { // create new version - project 3.2 // ////////////////////////////////////// - Item pr3_2 = versioningService.createNewVersion(context, pr3_1).getItem(); + Item pr3_2 = VersionBuilder.createVersion(context, pr3_1, "test").build().getItem(); installItemService.installItem(context, workspaceItemService.findByItem(context, pr3_2)); context.commit(); @@ -3056,7 +3053,7 @@ public void test_placeRecalculationNoUseForPlace() throws Exception { // create new version - volume 1.2 // ///////////////////////////////////// - Item v1_2 = versioningService.createNewVersion(context, v1_1).getItem(); + Item v1_2 = VersionBuilder.createVersion(context, v1_1, "test").build().getItem(); installItemService.installItem(context, workspaceItemService.findByItem(context, v1_2)); context.commit(); @@ -3064,7 +3061,7 @@ public void test_placeRecalculationNoUseForPlace() throws Exception { // create new version - issue 3.2 // //////////////////////////////////// - Item i3_2 = versioningService.createNewVersion(context, i3_1).getItem(); + Item i3_2 = VersionBuilder.createVersion(context, i3_1, "test").build().getItem(); installItemService.installItem(context, workspaceItemService.findByItem(context, i3_2)); context.commit(); @@ -3509,7 +3506,7 @@ public void test_virtualMetadataPreserved() throws Exception { // create a new version of publication 1 and archive // /////////////////////////////////////////////////////// - Item publication1V2 = versioningService.createNewVersion(context, publication1V1).getItem(); + Item publication1V2 = VersionBuilder.createVersion(context, publication1V1, "test").build().getItem(); installItemService.installItem(context, workspaceItemService.findByItem(context, publication1V2)); context.dispatchEvents(); @@ -3517,7 +3514,7 @@ public void test_virtualMetadataPreserved() throws Exception { // create new version of person 1 // //////////////////////////////////// - Item person1V2 = versioningService.createNewVersion(context, person1V1).getItem(); + Item person1V2 = VersionBuilder.createVersion(context, person1V1, "test").build().getItem(); // update "Smith, Donald" to "Smith, D." itemService.replaceMetadata( context, person1V2, "person", "givenName", null, null, "D.", @@ -3853,7 +3850,7 @@ public void test_virtualMetadataPreserved() throws Exception { // create new version of person 2 // //////////////////////////////////// - Item person2V2 = versioningService.createNewVersion(context, person2V1).getItem(); + Item person2V2 = VersionBuilder.createVersion(context, person2V1, "test").build().getItem(); Relationship rel1 = getRelationship(publication1V2, isAuthorOfPublication, person2V2); assertNotNull(rel1); rel1.setRightwardValue("Doe, Jane Jr"); diff --git a/dspace-api/src/test/java/org/dspace/content/WorkspaceItemTest.java b/dspace-api/src/test/java/org/dspace/content/WorkspaceItemTest.java index d018a15f9765..15d4720c9378 100644 --- a/dspace-api/src/test/java/org/dspace/content/WorkspaceItemTest.java +++ b/dspace-api/src/test/java/org/dspace/content/WorkspaceItemTest.java @@ -12,6 +12,7 @@ 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.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 void testSetPublishedBefore() { 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(); + } } diff --git a/dspace-api/src/test/java/org/dspace/content/dao/RelationshipDAOImplIT.java b/dspace-api/src/test/java/org/dspace/content/dao/RelationshipDAOImplIT.java index 2d08223b2e3e..20710ab5f25b 100644 --- a/dspace-api/src/test/java/org/dspace/content/dao/RelationshipDAOImplIT.java +++ b/dspace-api/src/test/java/org/dspace/content/dao/RelationshipDAOImplIT.java @@ -87,8 +87,8 @@ public void init() { WorkspaceItem workspaceItemTwo = workspaceItemService.create(context, collection, false); itemOne = installItemService.installItem(context, workspaceItem); itemTwo = installItemService.installItem(context, workspaceItemTwo); - itemService.addMetadata(context, itemOne, "dspace", "entity", "type", Item.ANY, "Publication"); - itemService.addMetadata(context, itemTwo, "dspace", "entity", "type", Item.ANY, "Person"); + itemService.addMetadata(context, itemOne, "dspace", "entity", "type", null, "Publication"); + itemService.addMetadata(context, itemTwo, "dspace", "entity", "type", null, "Person"); itemService.update(context, itemOne); itemService.update(context, itemTwo); entityTypeOne = entityTypeService.create(context, "Person"); diff --git a/dspace-api/src/test/java/org/dspace/content/dao/RelationshipTypeDAOImplIT.java b/dspace-api/src/test/java/org/dspace/content/dao/RelationshipTypeDAOImplIT.java index ff7d03b49f6d..d76e5faa804a 100644 --- a/dspace-api/src/test/java/org/dspace/content/dao/RelationshipTypeDAOImplIT.java +++ b/dspace-api/src/test/java/org/dspace/content/dao/RelationshipTypeDAOImplIT.java @@ -82,8 +82,8 @@ public void init() { WorkspaceItem workspaceItemTwo = workspaceItemService.create(context, collection, false); itemOne = installItemService.installItem(context, workspaceItem); itemTwo = installItemService.installItem(context, workspaceItemTwo); - itemService.addMetadata(context, itemOne, "dspace", "entity", "type", Item.ANY, "Publication"); - itemService.addMetadata(context, itemTwo, "dspace", "entity", "type", Item.ANY, "Person"); + itemService.addMetadata(context, itemOne, "dspace", "entity", "type", null, "Publication"); + itemService.addMetadata(context, itemTwo, "dspace", "entity", "type", null, "Person"); itemService.update(context, itemOne); itemService.update(context, itemTwo); entityTypeOne = entityTypeService.create(context, "Person"); diff --git a/dspace-api/src/test/java/org/dspace/content/service/ItemServiceIT.java b/dspace-api/src/test/java/org/dspace/content/service/ItemServiceIT.java index 0704c2d98d1c..02154e715c55 100644 --- a/dspace-api/src/test/java/org/dspace/content/service/ItemServiceIT.java +++ b/dspace-api/src/test/java/org/dspace/content/service/ItemServiceIT.java @@ -11,6 +11,7 @@ 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; @@ -198,7 +199,7 @@ public void preserveMetadataOrder() throws Exception { // now just add one metadata to be the last itemService.addMetadata( - context, item, dcSchema, contributorElement, authorQualifier, Item.ANY, "test, latest", null, 0 + context, item, dcSchema, contributorElement, authorQualifier, null, "test, latest", null, 0 ); // now just remove first metadata itemService.removeMetadataValues(context, item, List.of(placeZero)); @@ -990,4 +991,38 @@ public void testFindByMetadataQuery() throws Exception { 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(); + } + } diff --git a/dspace-api/src/test/java/org/dspace/core/ContextTest.java b/dspace-api/src/test/java/org/dspace/core/ContextTest.java index c6cd849d2110..ccc1d2f732cc 100644 --- a/dspace-api/src/test/java/org/dspace/core/ContextTest.java +++ b/dspace-api/src/test/java/org/dspace/core/ContextTest.java @@ -558,4 +558,29 @@ protected void init() { 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)); + } } diff --git a/dspace-api/src/test/java/org/dspace/core/EmailTest.java b/dspace-api/src/test/java/org/dspace/core/EmailTest.java new file mode 100644 index 000000000000..61b8dd31e1f0 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/core/EmailTest.java @@ -0,0 +1,68 @@ +/** + * 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.core; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; + +import java.io.IOException; + +import jakarta.mail.MessagingException; +import org.dspace.AbstractDSpaceTest; +import org.dspace.services.ConfigurationService; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests for email sender. + * + * @author mwood + */ +public class EmailTest + extends AbstractDSpaceTest { + private ConfigurationService config; + + @Before + public void init_test() { + config = kernelImpl.getConfigurationService(); + } + + @Test + public void testNullParameter() + throws MessagingException, IOException { + // Ensure that no mail goes out + config.setProperty("mail.server.disabled", "true"); + + Email email = new Email(); + email.setContent("null test", + "Testing: parameter value is /${params[0]}/."); + email.addArgument(null); + email.build(); + String message = email.getMessage(); + assertThat("Null message parameter should be transformed to empty", + message, not(containsString("(null)"))); + } + + @Test + public void testNotNullParameter() + throws MessagingException, IOException { + // Ensure that no mail goes out + config.setProperty("mail.server.disabled", "true"); + + Email email = new Email(); + email.setContent("not-null test", + "Testing: parameter value is /${params[0]}/."); + String testParam = "axolotl"; + email.addArgument(testParam); + email.build(); + String message = email.getMessage(); + assertThat("Null message parameter should be transformed to empty", + message, containsString(testParam)); + } +} diff --git a/dspace-api/src/test/java/org/dspace/core/HibernateDBConnectionTest.java b/dspace-api/src/test/java/org/dspace/core/HibernateDBConnectionTest.java index 093f693d567f..302844ce62ac 100644 --- a/dspace-api/src/test/java/org/dspace/core/HibernateDBConnectionTest.java +++ b/dspace-api/src/test/java/org/dspace/core/HibernateDBConnectionTest.java @@ -205,6 +205,28 @@ public void testReloadEntityAfterCommit() throws SQLException { .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 */ diff --git a/dspace-api/src/test/java/org/dspace/ctask/general/CreateMissingIdentifiersIT.java b/dspace-api/src/test/java/org/dspace/ctask/general/CreateMissingIdentifiersIT.java index 2a07799deee5..3b50258a5a23 100644 --- a/dspace-api/src/test/java/org/dspace/ctask/general/CreateMissingIdentifiersIT.java +++ b/dspace-api/src/test/java/org/dspace/ctask/general/CreateMissingIdentifiersIT.java @@ -11,7 +11,6 @@ import java.io.IOException; -import org.dspace.AbstractIntegrationTestWithDatabase; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; import org.dspace.builder.ItemBuilder; @@ -19,10 +18,11 @@ import org.dspace.content.Item; import org.dspace.core.factory.CoreServiceFactory; import org.dspace.curate.Curator; +import org.dspace.identifier.AbstractIdentifierProviderIT; +import org.dspace.identifier.VersionedHandleIdentifierProvider; import org.dspace.identifier.VersionedHandleIdentifierProviderWithCanonicalHandles; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.junit.After; import org.junit.Test; /** @@ -31,17 +31,19 @@ * @author mwood */ public class CreateMissingIdentifiersIT - extends AbstractIntegrationTestWithDatabase { + extends AbstractIdentifierProviderIT { + private static final String P_TASK_DEF = "plugin.named.org.dspace.curate.CurationTask"; private static final String TASK_NAME = "test"; + private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + @Test public void testPerform() throws IOException { // Must remove any cached named plugins before creating a new one CoreServiceFactory.getInstance().getPluginService().clearNamedPluginClasses(); - ConfigurationService configurationService = kernelImpl.getConfigurationService(); // Define a new task dynamically configurationService.setProperty(P_TASK_DEF, CreateMissingIdentifiers.class.getCanonicalName() + " = " + TASK_NAME); @@ -58,32 +60,27 @@ public void testPerform() .build(); /* - * Curate with regular test configuration -- should succeed. - */ - curator.curate(context, item); - int status = curator.getStatus(TASK_NAME); - assertEquals("Curation should succeed", Curator.CURATE_SUCCESS, status); - - /* - * Now install an incompatible provider to make the task fail. + * First, install an incompatible provider to make the task fail. */ - DSpaceServicesFactory.getInstance() - .getServiceManager() - .registerServiceClass( - VersionedHandleIdentifierProviderWithCanonicalHandles.class.getCanonicalName(), - VersionedHandleIdentifierProviderWithCanonicalHandles.class); + registerProvider(VersionedHandleIdentifierProviderWithCanonicalHandles.class); curator.curate(context, item); System.out.format("With incompatible provider, result is '%s'.\n", curator.getResult(TASK_NAME)); assertEquals("Curation should fail", Curator.CURATE_ERROR, curator.getStatus(TASK_NAME)); - } - @Override - @After - public void destroy() throws Exception { - super.destroy(); - DSpaceServicesFactory.getInstance().getServiceManager().getApplicationContext().refresh(); + // Unregister this non-default provider + unregisterProvider(VersionedHandleIdentifierProviderWithCanonicalHandles.class); + // Re-register the default provider (for later tests which may depend on it) + registerProvider(VersionedHandleIdentifierProvider.class); + + /* + * Now, verify curate with default Handle Provider works + * (and that our re-registration of the default provider above was successful) + */ + curator.curate(context, item); + int status = curator.getStatus(TASK_NAME); + assertEquals("Curation should succeed", Curator.CURATE_SUCCESS, status); } } diff --git a/dspace-api/src/test/java/org/dspace/discovery/DiscoveryIT.java b/dspace-api/src/test/java/org/dspace/discovery/DiscoveryIT.java index 6bc79cad558b..63ff93b6f387 100644 --- a/dspace-api/src/test/java/org/dspace/discovery/DiscoveryIT.java +++ b/dspace-api/src/test/java/org/dspace/discovery/DiscoveryIT.java @@ -8,6 +8,10 @@ package org.dspace.discovery; import static org.dspace.discovery.SolrServiceWorkspaceWorkflowRestrictionPlugin.DISCOVER_WORKSPACE_CONFIGURATION_NAME; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -21,6 +25,10 @@ import java.util.stream.Collectors; import jakarta.servlet.http.HttpServletRequest; +import org.apache.solr.client.solrj.SolrQuery; +import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.response.QueryResponse; +import org.apache.solr.common.SolrDocument; import org.dspace.AbstractIntegrationTestWithDatabase; import org.dspace.app.launcher.ScriptLauncher; import org.dspace.app.scripts.handler.impl.TestDSpaceRunnableHandler; @@ -99,6 +107,9 @@ public class DiscoveryIT extends AbstractIntegrationTestWithDatabase { MetadataAuthorityService metadataAuthorityService = ContentAuthorityServiceFactory.getInstance() .getMetadataAuthorityService(); + MockSolrSearchCore solrSearchCore = DSpaceServicesFactory.getInstance().getServiceManager() + .getServiceByName(null, MockSolrSearchCore.class); + @Override @Before public void setUp() throws Exception { @@ -796,6 +807,104 @@ public void searchWithDefaultSortServiceTest() throws SearchServiceException { } } + /** + * Test designed to check if the submitter is not indexed in all in solr documents for items + * and the submitter authority is still indexed + * @throws SearchServiceException + */ + @Test + public void searchWithNoSubmitterTest() throws SearchServiceException { + + configurationService.setProperty("discovery.index.item.submitter.enabled", false); + DiscoveryConfiguration defaultConf = SearchUtils.getDiscoveryConfiguration(context, "default", null); + + // Populate the testing objects: create items in eperson's workspace and perform search in it + int numberItems = 10; + context.turnOffAuthorisationSystem(); + EPerson submitter = null; + try { + submitter = EPersonBuilder.createEPerson(context).withEmail("submitter@example.org") + .withNameInMetadata("Peter", "Funny").build(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + context.setCurrentUser(submitter); + Community community = CommunityBuilder.createCommunity(context).build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + for (int i = 0; i < numberItems; i++) { + ItemBuilder.createItem(context, collection) + .withTitle("item " + i) + .build(); + } + context.restoreAuthSystemState(); + + // Build query with default parameters (except for workspaceConf) + QueryResponse result = null; + try { + result = solrSearchCore.getSolr().query(new SolrQuery(String.format( + "search.resourcetype:\"Item\""))); + } catch (SolrServerException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + assertEquals(result.getResults().size(), numberItems); + for (SolrDocument doc : result.getResults()) { + assertThat(doc.getFieldNames(), + not(hasItems("submitter_keyword", "submitter_ac", "submitter_acid", "submitter_filter"))); + assertThat(doc.getFieldNames(), hasItem("submitter_authority")); + } + } + + /** + * Test designed to check if the submitter is indexed in all in solr documents for items + * @throws SearchServiceException + */ + @Test + public void searchWithSubmitterTest() throws SearchServiceException { + + configurationService.setProperty("discovery.index.item.submitter.enabled", true); + DiscoveryConfiguration defaultConf = SearchUtils.getDiscoveryConfiguration(context, "default", null); + + // Populate the testing objects: create items in eperson's workspace and perform search in it + int numberItems = 10; + context.turnOffAuthorisationSystem(); + EPerson submitter = null; + try { + submitter = EPersonBuilder.createEPerson(context).withEmail("submitter@example.org") + .withNameInMetadata("Peter", "Funny").build(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + context.setCurrentUser(submitter); + Community community = CommunityBuilder.createCommunity(context).build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + for (int i = 0; i < numberItems; i++) { + ItemBuilder.createItem(context, collection) + .withTitle("item " + i) + .build(); + } + context.restoreAuthSystemState(); + + // Build query with default parameters (except for workspaceConf) + QueryResponse result = null; + try { + result = solrSearchCore.getSolr().query(new SolrQuery(String.format( + "search.resourcetype:\"Item\""))); + } catch (SolrServerException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + assertEquals(result.getResults().size(), numberItems); + for (SolrDocument doc : result.getResults()) { + for (String fieldname : doc.getFieldNames()) { + assertThat(doc.getFieldNames(), hasItems("submitter_keyword","submitter_ac", "submitter_filter", + "submitter_authority")); + } + } + } + private void assertSearchQuery(String resourceType, int size) throws SearchServiceException { assertSearchQuery(resourceType, size, size, 0, -1); } diff --git a/dspace-api/src/test/java/org/dspace/eperson/SubscribeServiceIT.java b/dspace-api/src/test/java/org/dspace/eperson/SubscribeServiceIT.java index 945dd481d00a..128b2e552b88 100644 --- a/dspace-api/src/test/java/org/dspace/eperson/SubscribeServiceIT.java +++ b/dspace-api/src/test/java/org/dspace/eperson/SubscribeServiceIT.java @@ -213,6 +213,7 @@ public void subscribingUserUnsubscribesTheirSubscription() throws Exception { secondCollection, 100, 0); assertEquals(subscriptions.size(), 1); + assertThat(subscribeService.isSubscribed(context, subscribingUser, secondCollection), is(true)); subscribeService.unsubscribe(context, subscribingUser, secondCollection); @@ -222,6 +223,7 @@ public void subscribingUserUnsubscribesTheirSubscription() throws Exception { secondCollection, 100, 0); assertEquals(subscriptions.size(), 0); + assertThat(subscribeService.isSubscribed(context, subscribingUser, secondCollection), is(false)); } @Test(expected = AuthorizeException.class) diff --git a/dspace-api/src/test/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProviderTest.java b/dspace-api/src/test/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProviderTest.java new file mode 100644 index 000000000000..a68c0519bae5 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProviderTest.java @@ -0,0 +1,234 @@ +/** + * 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.external.provider.impl; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.beans.HasPropertyWithValue.hasProperty; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.AllOf.allOf; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.InputStream; +import java.util.List; + +import org.dspace.AbstractDSpaceTest; +import org.dspace.external.OrcidRestConnector; +import org.dspace.external.model.ExternalDataObject; +import org.junit.Before; +import org.junit.Test; + + +/** + * Unit tests for {@link OrcidV3AuthorDataProvider}. + * + * @author Jesiel Viana (jesielviana at proton.me) + * + */ +public class OrcidV3AuthorDataProviderTest extends AbstractDSpaceTest { + + private static final String SEARCH_XML_PATH = "org/dspace/external/provider/orcid-v3-author/search.xml"; + private static final String PERSON1_XML_PATH = "org/dspace/external/provider/orcid-v3-author/person1.xml"; + private static final String PERSON2_XML_PATH = "org/dspace/external/provider/orcid-v3-author/person2.xml"; + private static final String PERSON3_XML_PATH = "org/dspace/external/provider/orcid-v3-author/person3.xml"; + + public static final String ORCID_SEARCH_QUERY = "search?q=0000-0000-0000-0000"; + + private OrcidV3AuthorDataProvider dataProvider; + + @Before + public void setup() throws Exception { + dataProvider = new OrcidV3AuthorDataProvider(); + + OrcidRestConnector mockRestConnector = mock(OrcidRestConnector.class); + + dataProvider.setOrcidRestConnector(mockRestConnector); + dataProvider.setSourceIdentifier("orcid"); + dataProvider.setOrcidUrl("https://orcid.org"); + + dataProvider.setClientId("client-id"); + dataProvider.setClientSecret("client-secret"); + dataProvider.setOAUTHUrl("https://orcid.org/oauth"); + + InputStream searchXmlStream = getClass().getClassLoader().getResourceAsStream(SEARCH_XML_PATH); + InputStream person1XmlStream = getClass().getClassLoader().getResourceAsStream(PERSON1_XML_PATH); + InputStream person2XmlStream = getClass().getClassLoader().getResourceAsStream(PERSON2_XML_PATH); + InputStream person3XmlStream = getClass().getClassLoader().getResourceAsStream(PERSON3_XML_PATH); + + when(mockRestConnector.get("search?q=search%3Fq%3D0000-0000-0000-0000&start=0&rows=10", null)) + .thenReturn(searchXmlStream); + when(mockRestConnector.get("0000-0000-0000-0001/person", null)).thenReturn(person1XmlStream); + when(mockRestConnector.get("0000-0000-0000-0002/person", null)).thenReturn(person2XmlStream); + when(mockRestConnector.get("0000-0000-0000-0003/person", null)).thenReturn(person3XmlStream); + + } + + @Test + public void testGetExternalDataObjectSizeIsCorrect() { + List optional = dataProvider.searchExternalDataObjects(ORCID_SEARCH_QUERY, 0, 10); + assertThat(optional, hasSize(3)); + } + + @Test + public void testGetExternalDataObjectGetPersonWithAllFieldsPopulated() { + List optional = dataProvider.searchExternalDataObjects(ORCID_SEARCH_QUERY, 0, 10); + + assertThat(optional, hasSize(3)); + + ExternalDataObject externalDataObject1 = optional.get(0); + + // Basic field assertions + assertThat(externalDataObject1.getId(), equalTo("0000-0000-0000-0001")); + assertThat(externalDataObject1.getValue(), equalTo("FamilyName1, GivenNames1")); + assertThat(externalDataObject1.getSource(), equalTo("orcid")); + assertThat(externalDataObject1.getDisplayValue(), equalTo("FamilyName1, GivenNames1")); + + // Metadata assertions + assertThat(externalDataObject1.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("familyName")), + hasProperty("value", equalTo("FamilyName1")) + ) + )); + assertThat(externalDataObject1.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("givenName")), + hasProperty("value", equalTo("GivenNames1")) + ) + )); + assertThat(externalDataObject1.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("email")), + hasProperty("value", equalTo("person1@email.com")) + ) + )); + assertThat(externalDataObject1.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("identifier")), + hasProperty("qualifier", equalTo("orcid")), + hasProperty("value", equalTo("0000-0000-0000-0001")) + ) + )); + assertThat(externalDataObject1.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("dc")), + hasProperty("element", equalTo("identifier")), + hasProperty("qualifier", equalTo("uri")), + hasProperty("value", equalTo("https://orcid.org/0000-0000-0000-0001")) + ) + )); + } + + @Test + public void testGetExternalDataObjectGetPrimaryEmailFromPersonWithTwoEmails() { + List optional = dataProvider.searchExternalDataObjects(ORCID_SEARCH_QUERY, 0, 10); + + assertThat(optional, hasSize(3)); + + ExternalDataObject externalDataObject2 = optional.get(1); // Test person2 (with two emails) + + // Basic field assertions + assertThat(externalDataObject2.getId(), equalTo("0000-0000-0000-0002")); + assertThat(externalDataObject2.getValue(), equalTo("FamilyName2, GivenNames2")); + assertThat(externalDataObject2.getSource(), equalTo("orcid")); + assertThat(externalDataObject2.getDisplayValue(), equalTo("FamilyName2, GivenNames2")); + + // Metadata assertions + assertThat(externalDataObject2.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("familyName")), + hasProperty("value", equalTo("FamilyName2")) + ) + )); + assertThat(externalDataObject2.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("givenName")), + hasProperty("value", equalTo("GivenNames2")) + ) + )); + assertThat(externalDataObject2.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("email")), + hasProperty("value", equalTo("person2primary@email.com")) // Primary email + ) + )); + assertThat(externalDataObject2.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("identifier")), + hasProperty("qualifier", equalTo("orcid")), + hasProperty("value", equalTo("0000-0000-0000-0002")) + ) + )); + assertThat(externalDataObject2.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("dc")), + hasProperty("element", equalTo("identifier")), + hasProperty("qualifier", equalTo("uri")), + hasProperty("value", equalTo("https://orcid.org/0000-0000-0000-0002")) + ) + )); + } + + + @Test + public void testGetExternalDataObjectGetPersonOnlyWithNameFilled() { + List optional = dataProvider.searchExternalDataObjects(ORCID_SEARCH_QUERY, 0, 10); + + assertThat(optional, hasSize(3)); + + ExternalDataObject externalDataObject2 = optional.get(2); // Test person2 (with two emails) + + // Basic field assertions + assertThat(externalDataObject2.getId(), equalTo("0000-0000-0000-0003")); + assertThat(externalDataObject2.getValue(), equalTo("FamilyName3, GivenNames3")); + assertThat(externalDataObject2.getSource(), equalTo("orcid")); + assertThat(externalDataObject2.getDisplayValue(), equalTo("FamilyName3, GivenNames3")); + + // Metadata assertions + assertThat(externalDataObject2.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("familyName")), + hasProperty("value", equalTo("FamilyName3")) + ) + )); + assertThat(externalDataObject2.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("givenName")), + hasProperty("value", equalTo("GivenNames3")) + ) + )); + assertThat(externalDataObject2.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("identifier")), + hasProperty("qualifier", equalTo("orcid")), + hasProperty("value", equalTo("0000-0000-0000-0003")) + ) + )); + assertThat(externalDataObject2.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("dc")), + hasProperty("element", equalTo("identifier")), + hasProperty("qualifier", equalTo("uri")), + hasProperty("value", equalTo("https://orcid.org/0000-0000-0000-0003")) + ) + )); + } +} diff --git a/dspace-api/src/test/java/org/dspace/identifier/AbstractIdentifierProviderIT.java b/dspace-api/src/test/java/org/dspace/identifier/AbstractIdentifierProviderIT.java new file mode 100644 index 000000000000..6ec9efddf5e2 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/identifier/AbstractIdentifierProviderIT.java @@ -0,0 +1,68 @@ +/** + * 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.identifier; + +import java.util.ArrayList; +import java.util.List; + +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.kernel.ServiceManager; +import org.dspace.services.factory.DSpaceServicesFactory; + +/** + * AbstractIdentifierProviderIT which contains a few useful utility methods for IdentifierProvider Integration Tests + */ +public class AbstractIdentifierProviderIT extends AbstractIntegrationTestWithDatabase { + + protected final ServiceManager serviceManager = DSpaceServicesFactory.getInstance().getServiceManager(); + protected final IdentifierServiceImpl identifierService = + serviceManager.getServicesByType(IdentifierServiceImpl.class).get(0); + + /** + * Register a specific IdentifierProvider into the current IdentifierService (replacing any existing providers). + * This method will also ensure the IdentifierProvider service is registered in the DSpace Service Manager. + * @param type IdentifierProvider Class + */ + protected void registerProvider(Class type) { + // Register our new provider + IdentifierProvider identifierProvider = + (IdentifierProvider) DSpaceServicesFactory.getInstance().getServiceManager() + .getServiceByName(type.getName(), type); + if (identifierProvider == null) { + DSpaceServicesFactory.getInstance().getServiceManager().registerServiceClass(type.getName(), type); + identifierProvider = (IdentifierProvider) DSpaceServicesFactory.getInstance().getServiceManager() + .getServiceByName(type.getName(), type); + } + + identifierService.setProviders(List.of(identifierProvider)); + } + + /** + * Unregister a specific IdentifierProvider from the current IdentifierService (removing all existing providers). + * This method will also ensure the IdentifierProvider service is unregistered in the DSpace Service Manager, + * which ensures it does not conflict with other IdentifierProvider services. + * @param type IdentifierProvider Class + */ + protected void unregisterProvider(Class type) { + // Find the provider service + IdentifierProvider identifierProvider = + (IdentifierProvider) DSpaceServicesFactory.getInstance().getServiceManager() + .getServiceByName(type.getName(), type); + // If found, unregister it + if (identifierProvider == null) { + DSpaceServicesFactory.getInstance().getServiceManager().unregisterService(type.getName()); + } + + // Overwrite the identifier-service's providers with an empty list + identifierService.setProviders(new ArrayList<>()); + } + +} + + + diff --git a/dspace-api/src/test/java/org/dspace/identifier/VersionedHandleIdentifierProviderIT.java b/dspace-api/src/test/java/org/dspace/identifier/VersionedHandleIdentifierProviderIT.java index 7e549f6cae33..3a1c8198e080 100644 --- a/dspace-api/src/test/java/org/dspace/identifier/VersionedHandleIdentifierProviderIT.java +++ b/dspace-api/src/test/java/org/dspace/identifier/VersionedHandleIdentifierProviderIT.java @@ -8,30 +8,28 @@ package org.dspace.identifier; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import java.sql.SQLException; -import java.util.ArrayList; import java.util.List; -import org.dspace.AbstractIntegrationTestWithDatabase; import org.dspace.authorize.AuthorizeException; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; import org.dspace.builder.ItemBuilder; import org.dspace.builder.VersionBuilder; import org.dspace.content.Collection; +import org.dspace.content.Community; import org.dspace.content.Item; -import org.dspace.kernel.ServiceManager; +import org.dspace.content.MetadataValue; +import org.dspace.content.factory.ContentServiceFactory; import org.dspace.services.factory.DSpaceServicesFactory; import org.junit.Before; import org.junit.Test; -public class VersionedHandleIdentifierProviderIT extends AbstractIntegrationTestWithDatabase { - private ServiceManager serviceManager; - private IdentifierServiceImpl identifierService; +public class VersionedHandleIdentifierProviderIT extends AbstractIdentifierProviderIT { private String firstHandle; + private String dspaceUrl; private Collection collection; private Item itemV1; @@ -44,27 +42,15 @@ public void setUp() throws Exception { super.setUp(); context.turnOffAuthorisationSystem(); - serviceManager = DSpaceServicesFactory.getInstance().getServiceManager(); - identifierService = serviceManager.getServicesByType(IdentifierServiceImpl.class).get(0); + dspaceUrl = DSpaceServicesFactory.getInstance().getConfigurationService().getProperty("dspace.ui.url"); // Clean out providers to avoid any being used for creation of community and collection - identifierService.setProviders(new ArrayList<>()); parentCommunity = CommunityBuilder.createCommunity(context) - .withName("Parent Community") - .build(); + .withName("Parent Community") + .build(); collection = CollectionBuilder.createCollection(context, parentCommunity) - .withName("Collection") - .build(); - } - - private void registerProvider(Class type) { - // Register our new provider - serviceManager.registerServiceClass(type.getName(), type); - IdentifierProvider identifierProvider = - (IdentifierProvider) serviceManager.getServiceByName(type.getName(), type); - - // Overwrite the identifier-service's providers with the new one to ensure only this provider is used - identifierService.setProviders(List.of(identifierProvider)); + .withName("Collection") + .build(); } private void createVersions() throws SQLException, AuthorizeException { @@ -78,7 +64,6 @@ private void createVersions() throws SQLException, AuthorizeException { @Test public void testDefaultVersionedHandleProvider() throws Exception { - registerProvider(VersionedHandleIdentifierProvider.class); createVersions(); // Confirm the original item only has its original handle @@ -93,23 +78,38 @@ public void testDefaultVersionedHandleProvider() throws Exception { } @Test - public void testCanonicalVersionedHandleProvider() throws Exception { - registerProvider(VersionedHandleIdentifierProviderWithCanonicalHandles.class); - createVersions(); + public void testCollectionHandleMetadata() { + registerProvider(VersionedHandleIdentifierProvider.class); - // Confirm the original item only has a version handle - assertEquals(firstHandle + ".1", itemV1.getHandle()); - assertEquals(1, itemV1.getHandles().size()); - // Confirm the second item has the correct version handle - assertEquals(firstHandle + ".2", itemV2.getHandle()); - assertEquals(1, itemV2.getHandles().size()); - // Confirm the last item has both the correct version handle and the original handle - assertEquals(firstHandle, itemV3.getHandle()); - assertEquals(2, itemV3.getHandles().size()); - containsHandle(itemV3, firstHandle + ".3"); + Community testCommunity = CommunityBuilder.createCommunity(context) + .withName("Test community") + .build(); + + Collection testCollection = CollectionBuilder.createCollection(context, testCommunity) + .withName("Test Collection") + .build(); + + List metadata = ContentServiceFactory.getInstance().getDSpaceObjectService(testCollection) + .getMetadata(testCollection, "dc", "identifier", "uri", + Item.ANY); + + assertEquals(1, metadata.size()); + assertEquals(dspaceUrl + "/handle/" + testCollection.getHandle(), metadata.get(0).getValue()); } - private void containsHandle(Item item, String handle) { - assertTrue(item.getHandles().stream().anyMatch(h -> handle.equals(h.getHandle()))); + @Test + public void testCommunityHandleMetadata() { + registerProvider(VersionedHandleIdentifierProvider.class); + + Community testCommunity = CommunityBuilder.createCommunity(context) + .withName("Test community") + .build(); + + List metadata = ContentServiceFactory.getInstance().getDSpaceObjectService(testCommunity) + .getMetadata(testCommunity, "dc", "identifier", "uri", + Item.ANY); + + assertEquals(1, metadata.size()); + assertEquals(dspaceUrl + "/handle/" + testCommunity.getHandle(), metadata.get(0).getValue()); } } diff --git a/dspace-api/src/test/java/org/dspace/identifier/VersionedHandleIdentifierProviderWithCanonicalHandlesIT.java b/dspace-api/src/test/java/org/dspace/identifier/VersionedHandleIdentifierProviderWithCanonicalHandlesIT.java new file mode 100644 index 000000000000..f6562d4db057 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/identifier/VersionedHandleIdentifierProviderWithCanonicalHandlesIT.java @@ -0,0 +1,139 @@ +/** + * 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.identifier; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.dspace.authorize.AuthorizeException; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.builder.VersionBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.kernel.ServiceManager; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.junit.Before; +import org.junit.Test; + +public class VersionedHandleIdentifierProviderWithCanonicalHandlesIT extends AbstractIdentifierProviderIT { + private ServiceManager serviceManager; + private IdentifierServiceImpl identifierService; + + private String firstHandle; + private String dspaceUrl; + + + private Collection collection; + private Item itemV1; + private Item itemV2; + private Item itemV3; + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + context.turnOffAuthorisationSystem(); + + serviceManager = DSpaceServicesFactory.getInstance().getServiceManager(); + dspaceUrl = DSpaceServicesFactory.getInstance().getConfigurationService().getProperty("dspace.ui.url"); + identifierService = serviceManager.getServicesByType(IdentifierServiceImpl.class).get(0); + // Clean out providers to avoid any being used for creation of community and collection + identifierService.setProviders(new ArrayList<>()); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection") + .build(); + + registerProvider(VersionedHandleIdentifierProviderWithCanonicalHandles.class); + } + + public void destroy() throws Exception { + super.destroy(); + // Unregister this non-default provider + unregisterProvider(VersionedHandleIdentifierProviderWithCanonicalHandles.class); + // Re-register the default provider (for later tests) + registerProvider(VersionedHandleIdentifierProvider.class); + } + + private void createVersions() throws SQLException, AuthorizeException { + itemV1 = ItemBuilder.createItem(context, collection) + .withTitle("First version") + .build(); + firstHandle = itemV1.getHandle(); + itemV2 = VersionBuilder.createVersion(context, itemV1, "Second version").build().getItem(); + itemV3 = VersionBuilder.createVersion(context, itemV1, "Third version").build().getItem(); + } + + @Test + public void testCanonicalVersionedHandleProvider() throws Exception { + createVersions(); + + // Confirm the original item only has a version handle + assertEquals(firstHandle + ".1", itemV1.getHandle()); + assertEquals(1, itemV1.getHandles().size()); + // Confirm the second item has the correct version handle + assertEquals(firstHandle + ".2", itemV2.getHandle()); + assertEquals(1, itemV2.getHandles().size()); + // Confirm the last item has both the correct version handle and the original handle + assertEquals(firstHandle, itemV3.getHandle()); + assertEquals(2, itemV3.getHandles().size()); + containsHandle(itemV3, firstHandle + ".3"); + } + + @Test + public void testCollectionHandleMetadata() { + registerProvider(VersionedHandleIdentifierProviderWithCanonicalHandles.class); + + Community testCommunity = CommunityBuilder.createCommunity(context) + .withName("Test community") + .build(); + + Collection testCollection = CollectionBuilder.createCollection(context, testCommunity) + .withName("Test Collection") + .build(); + + List metadata = ContentServiceFactory.getInstance().getDSpaceObjectService(testCollection) + .getMetadata(testCollection, "dc", "identifier", "uri", + Item.ANY); + + assertEquals(1, metadata.size()); + assertEquals(dspaceUrl + "/handle/" + testCollection.getHandle(), metadata.get(0).getValue()); + } + + @Test + public void testCommunityHandleMetadata() { + registerProvider(VersionedHandleIdentifierProviderWithCanonicalHandles.class); + + Community testCommunity = CommunityBuilder.createCommunity(context) + .withName("Test community") + .build(); + + List metadata = ContentServiceFactory.getInstance().getDSpaceObjectService(testCommunity) + .getMetadata(testCommunity, "dc", "identifier", "uri", + Item.ANY); + + assertEquals(1, metadata.size()); + assertEquals(dspaceUrl + "/handle/" + testCommunity.getHandle(), metadata.get(0).getValue()); + } + + private void containsHandle(Item item, String handle) { + assertTrue(item.getHandles().stream().anyMatch(h -> handle.equals(h.getHandle()))); + } +} diff --git a/dspace-api/src/test/java/org/dspace/orcid/OrcidQueueConsumerIT.java b/dspace-api/src/test/java/org/dspace/orcid/OrcidQueueConsumerIT.java index f2e528d78cd6..e17fd0072efa 100644 --- a/dspace-api/src/test/java/org/dspace/orcid/OrcidQueueConsumerIT.java +++ b/dspace-api/src/test/java/org/dspace/orcid/OrcidQueueConsumerIT.java @@ -23,6 +23,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; import java.sql.SQLException; import java.time.Instant; @@ -41,13 +42,19 @@ import org.dspace.content.Item; import org.dspace.content.MetadataValue; import org.dspace.content.RelationshipType; +import org.dspace.content.WorkspaceItem; import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.InstallItemService; import org.dspace.content.service.ItemService; +import org.dspace.content.service.WorkspaceItemService; import org.dspace.orcid.consumer.OrcidQueueConsumer; import org.dspace.orcid.factory.OrcidServiceFactory; import org.dspace.orcid.service.OrcidQueueService; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; +import org.dspace.utils.DSpace; +import org.dspace.versioning.Version; +import org.dspace.versioning.service.VersioningService; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -64,8 +71,15 @@ public class OrcidQueueConsumerIT extends AbstractIntegrationTestWithDatabase { private ItemService itemService = ContentServiceFactory.getInstance().getItemService(); + private WorkspaceItemService workspaceItemService = ContentServiceFactory.getInstance().getWorkspaceItemService(); + + private InstallItemService installItemService = ContentServiceFactory.getInstance().getInstallItemService(); + private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + private VersioningService versioningService = new DSpace().getServiceManager() + .getServicesByType(VersioningService.class).get(0); + private Collection profileCollection; @Before @@ -763,6 +777,177 @@ public void testWithManyInsertionAndDeletionOfSameMetadataValue() throws Excepti } + @Test + public void testOrcidQueueRecordCreationForPublicationWithNotFoundAuthority() throws Exception { + + context.turnOffAuthorisationSystem(); + + Item profile = ItemBuilder.createItem(context, profileCollection) + .withTitle("Test User") + .withOrcidIdentifier("0000-1111-2222-3333") + .withOrcidAccessToken("ab4d18a0-8d9a-40f1-b601-a417255c8d20", eperson) + .withOrcidSynchronizationPublicationsPreference(ALL) + .build(); + + Collection publicationCollection = createCollection("Publications", "Publication"); + + Item publication = ItemBuilder.createItem(context, publicationCollection) + .withTitle("Test publication") + .withAuthor("First User") + .withAuthor("Test User") + .build(); + + EntityType publicationType = EntityTypeBuilder.createEntityTypeBuilder(context, "Publication").build(); + EntityType personType = EntityTypeBuilder.createEntityTypeBuilder(context, "Person").build(); + + RelationshipType isAuthorOfPublication = createRelationshipTypeBuilder(context, personType, publicationType, + "isAuthorOfPublication", + "isPublicationOfAuthor", 0, null, 0, + null).build(); + + RelationshipBuilder.createRelationshipBuilder(context, profile, publication, isAuthorOfPublication).build(); + + context.restoreAuthSystemState(); + context.commit(); + + List orcidQueueRecords = orcidQueueService.findAll(context); + assertThat(orcidQueueRecords, hasSize(1)); + assertThat(orcidQueueRecords.get(0), matches(profile, publication, "Publication", INSERT)); + } + + @Test + public void testOrcidQueueWithItemVersioning() throws Exception { + + context.turnOffAuthorisationSystem(); + + Item profile = ItemBuilder.createItem(context, profileCollection) + .withTitle("Test User") + .withOrcidIdentifier("0000-1111-2222-3333") + .withOrcidAccessToken("ab4d18a0-8d9a-40f1-b601-a417255c8d20", eperson) + .withOrcidSynchronizationPublicationsPreference(ALL) + .build(); + + Collection publicationCollection = createCollection("Publications", "Publication"); + + Item publication = ItemBuilder.createItem(context, publicationCollection) + .withTitle("Test publication") + .withAuthor("Test User") + .build(); + + EntityType publicationType = EntityTypeBuilder.createEntityTypeBuilder(context, "Publication").build(); + EntityType personType = EntityTypeBuilder.createEntityTypeBuilder(context, "Person").build(); + + RelationshipType isAuthorOfPublication = createRelationshipTypeBuilder(context, personType, publicationType, + "isAuthorOfPublication", + "isPublicationOfAuthor", 0, null, 0, + null).build(); + + RelationshipBuilder.createRelationshipBuilder(context, profile, publication, isAuthorOfPublication).build(); + + context.restoreAuthSystemState(); + context.commit(); + + List orcidQueueRecords = orcidQueueService.findAll(context); + assertThat(orcidQueueRecords, hasSize(1)); + assertThat(orcidQueueRecords.get(0), matches(profile, publication, "Publication", INSERT)); + + context.turnOffAuthorisationSystem(); + Version newVersion = versioningService.createNewVersion(context, publication); + context.restoreAuthSystemState(); + Item newPublication = newVersion.getItem(); + assertThat(newPublication.isArchived(), is(false)); + + context.commit(); + + orcidQueueRecords = orcidQueueService.findAll(context); + assertThat(orcidQueueRecords, hasSize(1)); + assertThat(orcidQueueRecords.get(0), matches(profile, publication, "Publication", INSERT)); + + WorkspaceItem workspaceItem = workspaceItemService.findByItem(context, newVersion.getItem()); + context.turnOffAuthorisationSystem(); + + installItemService.installItem(context, workspaceItem); + + context.restoreAuthSystemState(); + context.commit(); + + orcidQueueRecords = orcidQueueService.findAll(context); + assertThat(orcidQueueRecords, hasSize(1)); + assertThat(orcidQueueRecords.get(0), matches(profile, newPublication, "Publication", INSERT)); + } + + @Test + public void testOrcidQueueUpdateWithItemVersioning() throws Exception { + + context.turnOffAuthorisationSystem(); + + Item profile = ItemBuilder.createItem(context, profileCollection) + .withTitle("Test User") + .withOrcidIdentifier("0000-1111-2222-3333") + .withOrcidAccessToken("ab4d18a0-8d9a-40f1-b601-a417255c8d20", eperson) + .withOrcidSynchronizationPublicationsPreference(ALL) + .build(); + + Collection publicationCollection = createCollection("Publications", "Publication"); + + Item publication = ItemBuilder.createItem(context, publicationCollection) + .withTitle("Test publication") + .build(); + + OrcidHistory orcidHistory = OrcidHistoryBuilder.createOrcidHistory(context, profile, publication) + .withDescription("Test publication") + .withOperation(OrcidOperation.INSERT) + .withPutCode("12345") + .withStatus(201) + .build(); + + addMetadata(publication, "dc", "contributor", "author", "Test User", null); + + EntityType publicationType = EntityTypeBuilder.createEntityTypeBuilder(context, "Publication").build(); + EntityType personType = EntityTypeBuilder.createEntityTypeBuilder(context, "Person").build(); + + RelationshipType isAuthorOfPublication = + createRelationshipTypeBuilder( + context, personType, publicationType, + "isAuthorOfPublication", + "isPublicationOfAuthor", 0, null, 0, + null + ).build(); + + RelationshipBuilder.createRelationshipBuilder(context, profile, publication, isAuthorOfPublication).build(); + + context.commit(); + + List orcidQueueRecords = orcidQueueService.findAll(context); + assertThat(orcidQueueRecords, hasSize(1)); + assertThat(orcidQueueRecords.get(0), matches(profile, publication, "Publication", "12345", UPDATE)); + + Version newVersion = versioningService.createNewVersion(context, publication); + Item newPublication = newVersion.getItem(); + assertThat(newPublication.isArchived(), is(false)); + + context.commit(); + + orcidQueueRecords = orcidQueueService.findAll(context); + assertThat(orcidQueueRecords, hasSize(1)); + assertThat(orcidQueueRecords.get(0), matches(profile, publication, "Publication", "12345", UPDATE)); + + WorkspaceItem workspaceItem = workspaceItemService.findByItem(context, newVersion.getItem()); + installItemService.installItem(context, workspaceItem); + + context.commit(); + + context.restoreAuthSystemState(); + + orcidQueueRecords = orcidQueueService.findAll(context); + assertThat(orcidQueueRecords, hasSize(1)); + assertThat(orcidQueueRecords.get(0), matches(profile, newPublication, "Publication", "12345", UPDATE)); + + orcidHistory = context.reloadEntity(orcidHistory); + assertThat(orcidHistory.getEntity(), is(newPublication)); + + } + private void addMetadata(Item item, String schema, String element, String qualifier, String value, String authority) throws Exception { context.turnOffAuthorisationSystem(); diff --git a/dspace-api/src/test/java/org/dspace/statistics/export/service/OpenUrlServiceImplTest.java b/dspace-api/src/test/java/org/dspace/statistics/export/service/OpenUrlServiceImplTest.java index d214050e6b5a..718ef701e136 100644 --- a/dspace-api/src/test/java/org/dspace/statistics/export/service/OpenUrlServiceImplTest.java +++ b/dspace-api/src/test/java/org/dspace/statistics/export/service/OpenUrlServiceImplTest.java @@ -27,9 +27,9 @@ import java.util.Date; import java.util.List; -import org.apache.http.HttpResponse; import org.apache.http.StatusLine; -import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.impl.client.CloseableHttpClient; import org.dspace.core.Context; import org.dspace.statistics.export.OpenURLTracker; import org.junit.Before; @@ -55,7 +55,7 @@ public class OpenUrlServiceImplTest { private FailedOpenURLTrackerService failedOpenURLTrackerService; @Mock - private HttpClient httpClient; + private CloseableHttpClient httpClient; @Before public void setUp() throws Exception { @@ -74,11 +74,11 @@ public void setUp() throws Exception { * @param statusCode the http status code to use in the mock. * @return a mocked http response. */ - protected HttpResponse createMockHttpResponse(int statusCode) { + protected CloseableHttpResponse createMockHttpResponse(int statusCode) { StatusLine statusLine = mock(StatusLine.class); when(statusLine.getStatusCode()).thenReturn(statusCode); - HttpResponse httpResponse = mock(HttpResponse.class); + CloseableHttpResponse httpResponse = mock(CloseableHttpResponse.class); when(httpResponse.getStatusLine()).thenReturn(statusLine); return httpResponse; diff --git a/dspace-api/src/test/java/org/dspace/supervision/SupervisionOrderServiceIT.java b/dspace-api/src/test/java/org/dspace/supervision/SupervisionOrderServiceIT.java index 60407823485b..aa4cd8bd4e49 100644 --- a/dspace-api/src/test/java/org/dspace/supervision/SupervisionOrderServiceIT.java +++ b/dspace-api/src/test/java/org/dspace/supervision/SupervisionOrderServiceIT.java @@ -18,6 +18,7 @@ import org.dspace.builder.CommunityBuilder; import org.dspace.builder.EPersonBuilder; import org.dspace.builder.GroupBuilder; +import org.dspace.builder.SupervisionOrderBuilder; import org.dspace.builder.WorkspaceItemBuilder; import org.dspace.content.Collection; import org.dspace.content.Item; @@ -85,10 +86,10 @@ public void createSupervisionOrderTest() throws Exception { .build(); SupervisionOrder supervisionOrderOne = - supervisionOrderService.create(context, item, groupA); + SupervisionOrderBuilder.createSupervisionOrder(context, item, groupA).build(); SupervisionOrder supervisionOrderTwo = - supervisionOrderService.create(context, item, groupB); + SupervisionOrderBuilder.createSupervisionOrder(context, item, groupB).build(); context.restoreAuthSystemState(); @@ -136,7 +137,8 @@ public void findSupervisionOrderTest() throws Exception { .build(); SupervisionOrder supervisionOrderOne = - supervisionOrderService.create(context, workspaceItem.getItem(), groupA); + SupervisionOrderBuilder.createSupervisionOrder(context, workspaceItem.getItem(), groupA) + .build(); context.restoreAuthSystemState(); @@ -205,9 +207,12 @@ public void findAllSupervisionOrdersTest() throws Exception { .addMember(userB) .build(); - supervisionOrderService.create(context, workspaceItem.getItem(), groupA); - supervisionOrderService.create(context, workspaceItem.getItem(), groupB); - supervisionOrderService.create(context, workspaceItemTwo.getItem(), groupA); + SupervisionOrderBuilder.createSupervisionOrder(context, workspaceItem.getItem(), groupA) + .build(); + SupervisionOrderBuilder.createSupervisionOrder(context, workspaceItem.getItem(), groupB) + .build(); + SupervisionOrderBuilder.createSupervisionOrder(context, workspaceItemTwo.getItem(), groupA) + .build(); context.restoreAuthSystemState(); @@ -259,9 +264,12 @@ public void findSupervisionOrderByItemTest() throws Exception { .addMember(eperson) .build(); - supervisionOrderService.create(context, workspaceItem.getItem(), groupA); - supervisionOrderService.create(context, workspaceItem.getItem(), groupB); - supervisionOrderService.create(context, workspaceItemTwo.getItem(), groupA); + SupervisionOrderBuilder.createSupervisionOrder(context, workspaceItem.getItem(), groupA) + .build(); + SupervisionOrderBuilder.createSupervisionOrder(context, workspaceItem.getItem(), groupB) + .build(); + SupervisionOrderBuilder.createSupervisionOrder(context, workspaceItemTwo.getItem(), groupA) + .build(); context.restoreAuthSystemState(); @@ -310,7 +318,8 @@ public void findSupervisionOrderByItemAndGroupTest() throws Exception { .addMember(eperson) .build(); - supervisionOrderService.create(context, item, groupA); + SupervisionOrderBuilder.createSupervisionOrder(context, item, groupA) + .build(); context.restoreAuthSystemState(); @@ -370,7 +379,8 @@ public void isSupervisorTest() throws Exception { .addMember(userB) .build(); - supervisionOrderService.create(context, workspaceItem.getItem(), groupA); + SupervisionOrderBuilder.createSupervisionOrder(context, workspaceItem.getItem(), groupA) + .build(); context.restoreAuthSystemState(); diff --git a/dspace-api/src/test/java/org/dspace/util/DSpaceKernelInitializer.java b/dspace-api/src/test/java/org/dspace/util/DSpaceKernelInitializer.java index a6f381bafbae..8f9169875ab3 100644 --- a/dspace-api/src/test/java/org/dspace/util/DSpaceKernelInitializer.java +++ b/dspace-api/src/test/java/org/dspace/util/DSpaceKernelInitializer.java @@ -83,6 +83,7 @@ public void initialize(final ConfigurableApplicationContext applicationContext) * Initially look for JNDI Resource called "java:/comp/env/dspace.dir". * If not found, use value provided in "dspace.dir" in Spring Environment */ + @SuppressWarnings("BanJNDI") private String getDSpaceHome(ConfigurableEnvironment environment) { // Load the "dspace.dir" property from Spring's configuration. // This gives us the location of our DSpace configuration, which is diff --git a/dspace-api/src/test/java/org/dspace/workflow/MockWorkflowItem.java b/dspace-api/src/test/java/org/dspace/workflow/MockWorkflowItem.java new file mode 100644 index 000000000000..d36ecf7331b8 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/workflow/MockWorkflowItem.java @@ -0,0 +1,62 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.workflow; + +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.eperson.EPerson; + +public class MockWorkflowItem implements WorkflowItem { + public Integer id; + public Item item; + public Collection collection; + public EPerson submitter; + boolean hasMultipleFiles; + boolean hasMultipleTitles; + boolean isPublishedBefore; + + public Integer getID() { + return id; + } + + public Item getItem() { + return item; + } + + public Collection getCollection() { + return collection; + } + + public EPerson getSubmitter() { + return submitter; + } + + public boolean hasMultipleFiles() { + return hasMultipleFiles; + } + + public void setMultipleFiles(boolean b) { + hasMultipleFiles = b; + } + + public boolean hasMultipleTitles() { + return hasMultipleTitles; + } + + public void setMultipleTitles(boolean b) { + hasMultipleTitles = b; + } + + public boolean isPublishedBefore() { + return isPublishedBefore; + } + + public void setPublishedBefore(boolean b) { + isPublishedBefore = b; + } +} diff --git a/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/person1.xml b/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/person1.xml new file mode 100644 index 000000000000..64e4b292b92b --- /dev/null +++ b/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/person1.xml @@ -0,0 +1,51 @@ + + + 2025-04-21T22:28:18.862Z + + 2025-04-11T15:41:21.340Z + 2025-04-11T15:41:21.340Z + GivenNames1 + FamilyName1 + + + + + 2025-04-21T22:28:18.862Z + + 2025-04-21T22:23:14.698Z + 2025-04-21T22:28:18.862Z + + + https://sandbox.orcid.org/0000-0000-0000-0001 + 0000-0000-0000-0001 + sandbox.orcid.org + + GivenNames1 FamilyName1 + + person1@email.com + + + + + + diff --git a/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/person2.xml b/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/person2.xml new file mode 100644 index 000000000000..c91b0207247d --- /dev/null +++ b/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/person2.xml @@ -0,0 +1,64 @@ + + + 2025-04-21T22:28:18.862Z + + 2025-04-11T15:41:21.340Z + 2025-04-11T15:41:21.340Z + GivenNames2 + FamilyName2 + + + + + 2025-04-21T22:28:18.862Z + + 2025-04-21T22:23:14.698Z + 2025-04-21T22:28:18.862Z + + + https://sandbox.orcid.org/0000-0000-0000-0002 + 0000-0000-0000-0002 + sandbox.orcid.org + + GivenNames2 FamilyName2 + + person2@email.com + + + 2025-04-21T16:42:54.961Z + 2025-04-21T16:48:32.642Z + + + https://sandbox.orcid.org/0000-0000-0000-0001 + 0000-0000-0000-0001 + sandbox.orcid.org + + GivenNames1 FamilyName1 + + person2primary@email.com + + + + + + diff --git a/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/person3.xml b/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/person3.xml new file mode 100644 index 000000000000..b24ed9d3547a --- /dev/null +++ b/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/person3.xml @@ -0,0 +1,35 @@ + + + + 2024-06-11T20:01:28.538Z + 2024-06-11T20:01:28.538Z + GivenNames3 + FamilyName3 + + + + + + + + diff --git a/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/search.xml b/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/search.xml new file mode 100644 index 000000000000..98ec721be9b8 --- /dev/null +++ b/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/search.xml @@ -0,0 +1,25 @@ + + + + + https://sandbox.orcid.org/0000-0000-0000-0001 + 0000-0000-0000-0001 + sandbox.orcid.org + + + + + https://sandbox.orcid.org/0000-0000-0000-0002 + 0000-0000-0000-0002 + sandbox.orcid.org + + + + + https://sandbox.orcid.org/0000-0000-0000-0003 + 0000-0000-0000-0003 + sandbox.orcid.org + + + diff --git a/dspace-iiif/pom.xml b/dspace-iiif/pom.xml index cd2b69288d2e..4a93ed41c006 100644 --- a/dspace-iiif/pom.xml +++ b/dspace-iiif/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 8.1-SNAPSHOT + 8.2 .. @@ -44,6 +44,11 @@ org.springframework.boot spring-boot-starter-logging + + + org.springframework + spring-jcl + @@ -106,7 +111,7 @@ de.digitalcollections.iiif iiif-apis - 0.3.10 + 0.3.11 org.javassist diff --git a/dspace-iiif/src/main/java/org/dspace/app/iiif/IIIFController.java b/dspace-iiif/src/main/java/org/dspace/app/iiif/IIIFController.java index 491a94d5658d..fd4cfe91d258 100644 --- a/dspace-iiif/src/main/java/org/dspace/app/iiif/IIIFController.java +++ b/dspace-iiif/src/main/java/org/dspace/app/iiif/IIIFController.java @@ -48,7 +48,7 @@ public class IIIFController { * @param id DSpace Item uuid * @return manifest as JSON */ - @RequestMapping(method = RequestMethod.GET, value = "/{id}/manifest") + @RequestMapping(method = RequestMethod.GET, value = "/{id}/manifest", produces = "application/json") public String findOne(@PathVariable UUID id) { Context context = ContextUtil.obtainCurrentRequestContext(); return iiifFacade.getManifest(context, id); diff --git a/dspace-oai/pom.xml b/dspace-oai/pom.xml index 4158ec74f34d..8a9bd8aa4044 100644 --- a/dspace-oai/pom.xml +++ b/dspace-oai/pom.xml @@ -8,15 +8,14 @@ dspace-parent org.dspace - 8.1-SNAPSHOT + 8.2 .. ${basedir}/.. - 3.4.0 - 5.87.0.RELEASE + 3.4.1 @@ -65,9 +64,8 @@ - javax.inject - javax.inject - 1 + jakarta.inject + jakarta.inject-api @@ -80,34 +78,24 @@ org.springframework.boot spring-boot-starter-logging - - - - - - org.jtwig - jtwig-spring-boot-starter - ${jtwig.version} - - + - org.springframework.boot - spring-boot-starter-web - - - - org.parboiled - parboiled-java + org.springframework + spring-jcl - + + + org.springframework + spring-webmvc + ${spring.version} + - org.parboiled - parboiled-java - 1.3.1 + org.springframework.boot + spring-boot-starter-thymeleaf + ${spring-boot.version} @@ -128,23 +116,6 @@ org.apache.logging.log4j log4j-api - - org.apache.logging.log4j - log4j-core - - - org.apache.logging.log4j - log4j-web - - - org.apache.logging.log4j - log4j-slf4j-impl - runtime - - - org.apache.logging.log4j - log4j-1.2-api - diff --git a/dspace-oai/src/main/java/org/dspace/app/configuration/OAIWebConfig.java b/dspace-oai/src/main/java/org/dspace/app/configuration/OAIWebConfig.java index dc4efde880d5..565d9f032632 100644 --- a/dspace-oai/src/main/java/org/dspace/app/configuration/OAIWebConfig.java +++ b/dspace-oai/src/main/java/org/dspace/app/configuration/OAIWebConfig.java @@ -11,8 +11,6 @@ import org.dspace.xoai.app.BasicConfiguration; import org.dspace.xoai.services.api.xoai.ItemRepositoryResolver; import org.dspace.xoai.services.impl.xoai.DSpaceItemRepositoryResolver; -import org.jtwig.spring.JtwigViewResolver; -import org.jtwig.spring.boot.config.JtwigViewResolverConfigurer; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -20,31 +18,37 @@ import org.springframework.context.annotation.Import; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.thymeleaf.spring6.SpringTemplateEngine; +import org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver; +import org.thymeleaf.spring6.view.ThymeleafViewResolver; +import org.thymeleaf.templatemode.TemplateMode; /** - * OAI-PMH webapp configuration. Replaces the old web.xml + * OAI-PMH webapp configuration. Replaces the old web.xml. + * This webapp used JTwig in earlier versions and has been refactored to + * use Thymeleaf instead. *

* This @Configuration class is automatically discovered by Spring Boot via a @ComponentScan * on the org.dspace.app.configuration package. *

* * - * @author Tim Donohue + * @author Kim Shepherd */ @Configuration // Import additional configuration and beans from BasicConfiguration @Import(BasicConfiguration.class) // Scan for controllers in this package @ComponentScan("org.dspace.xoai.controller") -public class OAIWebConfig implements WebMvcConfigurer, JtwigViewResolverConfigurer { +public class OAIWebConfig implements WebMvcConfigurer { // Path where OAI is deployed. Defaults to "oai" // NOTE: deployment on this path is handled by org.dspace.xoai.controller.DSpaceOAIDataProvider @Value("${oai.path:oai}") private String oaiPath; - private static final String TWIG_HTML_EXTENSION = ".twig.html"; private static final String VIEWS_LOCATION = "classpath:/templates/"; + private static final String HTML_EXTENSION = ".html"; /** * Ensure all resources under src/main/resources/static/ directory are available @@ -58,18 +62,42 @@ public void addResourceHandlers(ResourceHandlerRegistry registry) { } /** - * Configure the Jtwig template engine for Spring Boot - * Ensures Jtwig looks for templates in proper location with proper extension + * Configure the Thymeleaf template resolver **/ - @Override - public void configure(JtwigViewResolver viewResolver) { - viewResolver.setPrefix(VIEWS_LOCATION); - viewResolver.setSuffix(TWIG_HTML_EXTENSION); + @Bean + public SpringResourceTemplateResolver templateResolver() { + SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver(); + templateResolver.setPrefix(VIEWS_LOCATION); + templateResolver.setSuffix(HTML_EXTENSION); + templateResolver.setTemplateMode(TemplateMode.HTML); + templateResolver.setCacheable(true); + return templateResolver; + } + + /** + * Configure the Thymeleaf template engine + **/ + @Bean + public SpringTemplateEngine templateEngine() { + SpringTemplateEngine templateEngine = new SpringTemplateEngine(); + templateEngine.setTemplateResolver(templateResolver()); + templateEngine.setEnableSpringELCompiler(true); + return templateEngine; + } + + /** + * Configure the Thymeleaf view resolver + **/ + @Bean + public ThymeleafViewResolver viewResolver() { + ThymeleafViewResolver viewResolver = new ThymeleafViewResolver(); + viewResolver.setTemplateEngine(templateEngine()); + viewResolver.setCharacterEncoding("UTF-8"); + return viewResolver; } @Bean public ItemRepositoryResolver xoaiItemRepositoryResolver() { return new DSpaceItemRepositoryResolver(); } -} - +} \ No newline at end of file diff --git a/dspace-oai/src/main/java/org/dspace/xoai/app/CCElementItemCompilePlugin.java b/dspace-oai/src/main/java/org/dspace/xoai/app/CCElementItemCompilePlugin.java index 225d56a4c982..370543029d8b 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/app/CCElementItemCompilePlugin.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/app/CCElementItemCompilePlugin.java @@ -11,7 +11,7 @@ import com.lyncode.xoai.dataprovider.xml.xoai.Element; import com.lyncode.xoai.dataprovider.xml.xoai.Metadata; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.dspace.content.Item; import org.dspace.core.Context; import org.dspace.license.factory.LicenseServiceFactory; diff --git a/dspace-oai/src/main/java/org/dspace/xoai/app/XOAI.java b/dspace-oai/src/main/java/org/dspace/xoai/app/XOAI.java index 25cc1ee3655f..c8babc036e3c 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/app/XOAI.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/app/XOAI.java @@ -9,7 +9,7 @@ import static com.lyncode.xoai.dataprovider.core.Granularity.Second; import static java.util.Objects.nonNull; -import static org.apache.commons.lang.StringUtils.EMPTY; +import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.apache.solr.common.params.CursorMarkParams.CURSOR_MARK_PARAM; import static org.apache.solr.common.params.CursorMarkParams.CURSOR_MARK_START; import static org.dspace.xoai.util.ItemUtils.retrieveMetadata; @@ -110,7 +110,7 @@ private List getFileFormats(Item item) { try { for (Bundle b : itemService.getBundles(item, "ORIGINAL")) { for (Bitstream bs : b.getBitstreams()) { - if (!formats.contains(bs.getFormat(context).getMIMEType())) { + if (bs != null && !formats.contains(bs.getFormat(context).getMIMEType())) { formats.add(bs.getFormat(context).getMIMEType()); } } @@ -334,6 +334,11 @@ private int index(Iterator iterator) throws DSpaceSolrIndexerException { server.add(list); server.commit(); list.clear(); + try { + context.uncacheEntities(); + } catch (SQLException ex) { + log.error("Error uncaching entities", ex); + } } } System.out.println("Total: " + i + " items"); diff --git a/dspace-oai/src/main/java/org/dspace/xoai/filter/ItemsWithBitstreamFilter.java b/dspace-oai/src/main/java/org/dspace/xoai/filter/ItemsWithBitstreamFilter.java index 3599c5b9e168..9bf1c65dc9d9 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/filter/ItemsWithBitstreamFilter.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/filter/ItemsWithBitstreamFilter.java @@ -9,8 +9,8 @@ import java.sql.SQLException; -import org.apache.log4j.LogManager; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.Bundle; import org.dspace.content.Item; import org.dspace.handle.factory.HandleServiceFactory; diff --git a/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java b/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java index 20dcabcb20c8..40a193ea2905 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java @@ -194,7 +194,8 @@ private static void addResourcePolicyInformation(Context context, Bitstream bits resourcePolicyEl.getField().add(createValue("group", groupName)); resourcePolicyEl.getField().add(createValue("user", user)); resourcePolicyEl.getField().add(createValue("action", action)); - if (startDate != null) { + // Only add start-date if group is different to anonymous, or there is an active embargo + if (startDate != null && startDate.after(new Date())) { resourcePolicyEl.getField().add(createValue("start-date", formatter.format(startDate))); } if (endDate != null) { diff --git a/dspace-oai/src/main/resources/static/style.xsl b/dspace-oai/src/main/resources/static/style.xsl index 17eb865e8f1f..3830bf4228b0 100644 --- a/dspace-oai/src/main/resources/static/style.xsl +++ b/dspace-oai/src/main/resources/static/style.xsl @@ -319,20 +319,22 @@ -

- - # -
-
Metadata
-
-
-
- -
- -
-
-
+ +
+ + # +
+
Metadata
+
+
+
+ +
+ +
+
+
+
@@ -378,14 +380,16 @@ -
-
-
Metadata
-
-
- -
-
+ +
+
+
Metadata
+
+
+ +
+
+
@@ -522,15 +526,14 @@ - + - - + - - + diff --git a/dspace-oai/src/main/resources/templates/index.html b/dspace-oai/src/main/resources/templates/index.html new file mode 100644 index 000000000000..a0d35d91dea2 --- /dev/null +++ b/dspace-oai/src/main/resources/templates/index.html @@ -0,0 +1,78 @@ + + + + + + DSpace OAI-PMH Data Provider + + + + + + + + + + + +
+ + + + + + + +
+

Available Contexts

+ +
+ +
+
+

DSpace OAI-PMH Data Provider

+

+ + DSpace + +

+
+ +
+ + diff --git a/dspace-oai/src/main/resources/templates/index.twig.html b/dspace-oai/src/main/resources/templates/index.twig.html deleted file mode 100644 index c473188e35c0..000000000000 --- a/dspace-oai/src/main/resources/templates/index.twig.html +++ /dev/null @@ -1,81 +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/ - -#} -{# - - DSpace OAI default index template. To override this template, place a customized version in - the [webapp]/WEB-INF/classes/templates/ folder, and reboot your servlet engine. - -#} - - - - DSpace OAI-PMH Data Provider - - - - - {# NOTE: We use JQuery and Bootstrap via WebJars which are configured in dspace-server-webapp #} - - - - - - -
- - - - - - - -
-

Available Contexts

-
- {% for item in contexts %} -
-

{{ item.name }} {% if (item.description) %}{{ item.description }}{% endif %}

-

-

-

-

-
- {% endfor %} -
-
- -
-
-

DSpace OAI-PMH Data Provider

-

- - DSpace - -

-
- -
- - diff --git a/dspace-oai/src/test/java/org/dspace/xoai/tests/integration/xoai/PipelineTest.java b/dspace-oai/src/test/java/org/dspace/xoai/tests/integration/xoai/PipelineTest.java index 0f48824159c2..0f7ffde0bd00 100644 --- a/dspace-oai/src/test/java/org/dspace/xoai/tests/integration/xoai/PipelineTest.java +++ b/dspace-oai/src/test/java/org/dspace/xoai/tests/integration/xoai/PipelineTest.java @@ -13,13 +13,14 @@ import static org.hamcrest.MatcherAssert.assertThat; import java.io.InputStream; +import java.nio.charset.Charset; import javax.xml.transform.TransformerFactory; import javax.xml.transform.stream.StreamSource; import com.lyncode.xoai.util.XSLPipeline; +import org.apache.commons.io.IOUtils; import org.dspace.xoai.tests.support.XmlMatcherBuilder; import org.junit.Test; -import org.parboiled.common.FileUtils; public class PipelineTest { private static TransformerFactory factory = TransformerFactory.newInstance(); @@ -28,9 +29,9 @@ public class PipelineTest { public void pipelineTest() throws Exception { InputStream input = PipelineTest.class.getClassLoader().getResourceAsStream("item.xml"); InputStream xslt = PipelineTest.class.getClassLoader().getResourceAsStream("oai_dc.xsl"); - String output = FileUtils.readAllText(new XSLPipeline(input, true) - .apply(factory.newTemplates(new StreamSource(xslt))) - .getTransformed()); + String output = IOUtils.toString(new XSLPipeline(input, true) + .apply(factory.newTemplates(new StreamSource(xslt))) + .getTransformed(), Charset.defaultCharset()); assertThat(output, oai_dc().withXPath("/oai_dc:dc/dc:title", equalTo("Teste"))); diff --git a/dspace-rdf/pom.xml b/dspace-rdf/pom.xml index d7db7fe76e8a..f18177116b45 100644 --- a/dspace-rdf/pom.xml +++ b/dspace-rdf/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 8.1-SNAPSHOT + 8.2 .. @@ -67,6 +67,11 @@ org.springframework.boot spring-boot-starter-logging + + + org.springframework + spring-jcl +
@@ -80,14 +85,6 @@ org.apache.logging.log4j log4j-api - - org.apache.logging.log4j - log4j-core - - - org.apache.logging.log4j - log4j-web - org.apache.commons diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 563019983402..e38e99a30cb2 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -14,7 +14,7 @@ org.dspace dspace-parent - 8.1-SNAPSHOT + 8.2 .. @@ -31,7 +31,7 @@ org.codehaus.mojo properties-maven-plugin - 1.1.0 + 1.2.1 initialize @@ -293,14 +293,6 @@ spring-expression ${spring.version} - - - @@ -343,6 +335,18 @@ org.springframework.boot spring-boot-starter-actuator ${spring-boot.version} + + + + io.micrometer + micrometer-observation + + + + io.micrometer + micrometer-commons + + @@ -419,7 +423,7 @@ org.webjars.npm json-editor__json-editor - 2.6.1 + 2.15.2 + io.micrometer micrometer-observation @@ -456,6 +460,11 @@ org.springframework.boot spring-boot-starter-logging + + + org.springframework + spring-jcl + @@ -543,7 +552,7 @@ net.minidev json-smart - 2.5.0 + 2.5.2 @@ -581,7 +590,7 @@ org.apache.httpcomponents.client5 httpclient5 - 5.3.1 + 5.5 test @@ -593,6 +602,13 @@ com.jayway.jsonpath json-path + + + + net.minidev + json-smart + + com.jayway.jsonpath diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java index 070f3d8a1868..63ac50b6ea06 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java @@ -220,7 +220,7 @@ private AuthenticationTokenResource shortLivedTokenResponse(HttpServletRequest r * @return ResponseEntity */ @RequestMapping(value = "/login", method = { RequestMethod.GET, RequestMethod.PUT, RequestMethod.PATCH, - RequestMethod.DELETE }) + RequestMethod.DELETE }) public ResponseEntity login() { return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body("Only POST is allowed for login requests."); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java index db9d26a5f6ca..11b048e23ef1 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java @@ -135,11 +135,16 @@ public ResponseEntity retrieve(@PathVariable UUID uuid, HttpServletResponse resp long filesize = bit.getSizeBytes(); Boolean citationEnabledForBitstream = citationDocumentService.isCitationEnabledForBitstream(bit, context); + var bitstreamResource = + new org.dspace.app.rest.utils.BitstreamResource(name, uuid, + currentUser != null ? currentUser.getID() : null, + context.getSpecialGroupUuids(), citationEnabledForBitstream); + HttpHeadersInitializer httpHeadersInitializer = new HttpHeadersInitializer() .withBufferSize(BUFFER_SIZE) .withFileName(name) - .withChecksum(bit.getChecksum()) - .withLength(bit.getSizeBytes()) + .withChecksum(bitstreamResource.getChecksum()) + .withLength(bitstreamResource.contentLength()) .withMimetype(mimetype) .with(request) .with(response); @@ -157,11 +162,6 @@ public ResponseEntity retrieve(@PathVariable UUID uuid, HttpServletResponse resp httpHeadersInitializer.withDisposition(HttpHeadersInitializer.CONTENT_DISPOSITION_ATTACHMENT); } - org.dspace.app.rest.utils.BitstreamResource bitstreamResource = - new org.dspace.app.rest.utils.BitstreamResource(name, uuid, - currentUser != null ? currentUser.getID() : null, - context.getSpecialGroupUuids(), citationEnabledForBitstream); - //We have all the data we need, close the connection to the database so that it doesn't stay open during //download/streaming context.complete(); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ItemOwningCollectionUpdateRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ItemOwningCollectionUpdateRestController.java index eec5b15825ac..db238e1a5c83 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ItemOwningCollectionUpdateRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ItemOwningCollectionUpdateRestController.java @@ -43,7 +43,7 @@ import org.springframework.web.bind.annotation.RestController; /** - * This controller will handle all the incoming calls on the api/code/items/{uuid}/owningCollection endpoint + * This controller will handle all the incoming calls on the api/core/items/{uuid}/owningCollection endpoint * where the uuid corresponds to the item of which you want to edit the owning collection. */ @RestController diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/OpenSearchController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/OpenSearchController.java index baf45c14b6eb..85ec441e20c1 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/OpenSearchController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/OpenSearchController.java @@ -21,17 +21,13 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.app.rest.utils.ScopeResolver; import org.dspace.app.util.SyndicationFeed; import org.dspace.app.util.factory.UtilServiceFactory; import org.dspace.app.util.service.OpenSearchService; -import org.dspace.authorize.factory.AuthorizeServiceFactory; -import org.dspace.authorize.service.AuthorizeService; -import org.dspace.content.factory.ContentServiceFactory; -import org.dspace.content.service.CollectionService; -import org.dspace.content.service.CommunityService; import org.dspace.core.Context; import org.dspace.core.LogHelper; import org.dspace.core.Utils; @@ -50,7 +46,6 @@ import org.dspace.discovery.indexobject.IndexableItem; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -67,12 +62,9 @@ public class OpenSearchController { private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(); - private static final String errorpath = "/error"; + private List searchIndices = null; - private CommunityService communityService; - private CollectionService collectionService; - private AuthorizeService authorizeService; private OpenSearchService openSearchService; @Autowired @@ -99,22 +91,28 @@ public void search(HttpServletRequest request, @RequestParam(name = "format", required = false) String format, @RequestParam(name = "sort", required = false) String sort, @RequestParam(name = "sort_direction", required = false) String sortDirection, - @RequestParam(name = "scope", required = false) String dsoObject, - Model model) throws IOException, ServletException { + @RequestParam(name = "scope", required = false) String dsoObject) + throws IOException, ServletException { context = ContextUtil.obtainContext(request); - if (start == null) { - start = 0; - } - if (count == null) { - count = -1; - } + if (openSearchService == null) { openSearchService = UtilServiceFactory.getInstance().getOpenSearchService(); } + if (openSearchService.isEnabled()) { init(); + + if (start == null) { + start = 0; + } + + if (count == null) { + count = -1; + } + count = Math.min(count, openSearchService.getMaxNumOfItemsPerRequest()); + // get enough request parameters to decide on action to take - if (format == null || "".equals(format)) { + if (StringUtils.isEmpty(format)) { // default to atom format = "atom"; } @@ -266,9 +264,6 @@ private void init() { searchIndices.add(sFilter.getIndexFieldName()); } } - communityService = ContentServiceFactory.getInstance().getCommunityService(); - collectionService = ContentServiceFactory.getInstance().getCollectionService(); - authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService(); } public void setOpenSearchService(OpenSearchService oSS) { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RootRestResourceController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RootRestResourceController.java index d6ed84c3d656..51ef5ea5837a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RootRestResourceController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RootRestResourceController.java @@ -43,6 +43,6 @@ public class RootRestResourceController { @RequestMapping(method = RequestMethod.GET) public RootResource listDefinedEndpoint(HttpServletRequest request) { - return converter.toResource(rootRestRepository.getRoot()); + return converter.toResource(rootRestRepository.getRoot(request)); } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/UUIDLookupRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/UUIDLookupRestController.java index 8a35794aa193..3da84cc2d372 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/UUIDLookupRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/UUIDLookupRestController.java @@ -24,9 +24,11 @@ import org.dspace.app.rest.utils.ContextUtil; import org.dspace.app.rest.utils.DSpaceObjectUtils; import org.dspace.app.rest.utils.Utils; +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.DSpaceObject; +import org.dspace.core.Constants; import org.dspace.core.Context; -import org.dspace.discovery.SearchServiceException; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.hateoas.Link; @@ -65,6 +67,9 @@ public class UUIDLookupRestController implements InitializingBean { @Autowired private DiscoverableEndpointsService discoverableEndpointsService; + @Autowired + private AuthorizeService authorizeService; + @Autowired private ConverterService converter; @@ -85,13 +90,14 @@ public void afterPropertiesSet() throws Exception { public void getDSObyIdentifier(HttpServletRequest request, HttpServletResponse response, @RequestParam(PARAM) UUID uuid) - throws IOException, SQLException, SearchServiceException { + throws IOException, SQLException, AuthorizeException { Context context = null; try { context = ContextUtil.obtainContext(request); DSpaceObject dso = dspaceObjectUtil.findDSpaceObject(context, uuid); if (dso != null) { + authorizeService.authorizeAction(context, dso, Constants.READ); DSpaceObjectRest dsor = converter.toRest(dso, utils.obtainProjection()); URI link = linkTo(dsor.getController(), dsor.getCategory(), dsor.getTypePlural()).slash(dsor.getId()) .toUri(); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java index 08a7e9aec8e9..670cff84581e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java @@ -14,6 +14,7 @@ import org.apache.solr.client.solrj.SolrServerException; import org.dspace.app.rest.DiscoverableEndpointsService; import org.dspace.app.rest.health.GeoIpHealthIndicator; +import org.dspace.app.rest.health.SEOHealthIndicator; import org.dspace.app.rest.health.SolrHealthIndicator; import org.dspace.authority.AuthoritySolrServiceImpl; import org.dspace.discovery.SolrSearchCore; @@ -82,6 +83,12 @@ public SolrHealthIndicator solrOaiCoreHealthIndicator(SolrServerResolver solrSer return new SolrHealthIndicator(solrServerResolver.getServer()); } + @Bean + @ConditionalOnEnabledHealthIndicator("seo") + public SEOHealthIndicator seoHealthIndicator() { + return new SEOHealthIndicator(); + } + @Bean @ConditionalOnEnabledHealthIndicator("geoIp") public GeoIpHealthIndicator geoIpHealthIndicator() { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemConverter.java index fc64b66e8a16..a1e9442f7466 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemConverter.java @@ -8,7 +8,6 @@ package org.dspace.app.rest.converter; import java.sql.SQLException; -import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Objects; @@ -64,6 +63,9 @@ public ItemRest convert(Item obj, Projection projection) { /** * Retrieves the metadata list filtered according to the hidden metadata configuration * When the context is null, it will return the metadatalist as for an anonymous user + * When the context is not null, it will return the full metadata list if the user + * is allowed to edit the item or if the user is an admin. Otherwise, it will + * return the metadata list filtered according to the hidden metadata configuration * Overrides the parent method to include virtual metadata * @param context The context * @param obj The object of which the filtered metadata will be retrieved @@ -76,8 +78,9 @@ public MetadataValueList getPermissionFilteredMetadata(Context context, Item obj List returnList = new LinkedList<>(); try { if (obj.isWithdrawn() && (Objects.isNull(context) || - Objects.isNull(context.getCurrentUser()) || !authorizeService.isAdmin(context))) { - return new MetadataValueList(new ArrayList()); + Objects.isNull(context.getCurrentUser()) || + !authorizeService.isAdmin(context, obj))) { + return new MetadataValueList(List.of()); } if (context != null && (authorizeService.isAdmin(context) || itemService.canEdit(context, obj))) { return new MetadataValueList(fullList); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RootConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RootConverter.java index 61f18a5b3c9c..bfc125fd8672 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RootConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RootConverter.java @@ -9,6 +9,7 @@ import static org.dspace.app.util.Util.getSourceVersion; +import jakarta.servlet.http.HttpServletRequest; import org.dspace.app.rest.model.RootRest; import org.dspace.services.ConfigurationService; import org.springframework.beans.factory.annotation.Autowired; @@ -23,12 +24,21 @@ public class RootConverter { @Autowired private ConfigurationService configurationService; - public RootRest convert() { + public RootRest convert(HttpServletRequest request) { RootRest rootRest = new RootRest(); rootRest.setDspaceName(configurationService.getProperty("dspace.name")); rootRest.setDspaceUI(configurationService.getProperty("dspace.ui.url")); - rootRest.setDspaceServer(configurationService.getProperty("dspace.server.url")); + String requestUrl = request.getRequestURL().toString(); + String dspaceUrl = configurationService.getProperty("dspace.server.url"); + String dspaceSSRUrl = configurationService.getProperty("dspace.server.ssr.url", dspaceUrl); + if (!dspaceUrl.equals(dspaceSSRUrl) && requestUrl.startsWith(dspaceSSRUrl)) { + rootRest.setDspaceServer(dspaceSSRUrl); + } else { + rootRest.setDspaceServer(dspaceUrl); + } rootRest.setDspaceVersion("DSpace " + getSourceVersion()); return rootRest; } + + } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SearchEventConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SearchEventConverter.java index e9786962e0f5..dcf42f099821 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SearchEventConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SearchEventConverter.java @@ -13,7 +13,8 @@ import java.util.List; import jakarta.servlet.http.HttpServletRequest; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.PageRest; import org.dspace.app.rest.model.SearchEventRest; import org.dspace.app.rest.model.SearchResultsRest; @@ -31,7 +32,7 @@ @Component public class SearchEventConverter { /* Log4j logger */ - private static final Logger log = Logger.getLogger(SearchEventConverter.class); + private static final Logger log = LogManager.getLogger(SearchEventConverter.class); @Autowired private ScopeResolver scopeResolver; @@ -66,8 +67,8 @@ public UsageSearchEvent convert(Context context, HttpServletRequest request, Sea if (searchEventRest.getScope() != null) { IndexableObject scopeObject = scopeResolver.resolveScope(context, String.valueOf(searchEventRest.getScope())); - if (scopeObject instanceof DSpaceObject) { - usageSearchEvent.setScope((DSpaceObject) scopeObject); + if (scopeObject != null && scopeObject.getIndexedObject() instanceof DSpaceObject) { + usageSearchEvent.setScope((DSpaceObject) scopeObject.getIndexedObject()); } } usageSearchEvent.setConfiguration(searchEventRest.getConfiguration()); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionSectionConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionSectionConverter.java index 0391cbce7a2d..3cd263493b5d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionSectionConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionSectionConverter.java @@ -10,6 +10,7 @@ import java.sql.SQLException; import org.apache.logging.log4j.Logger; +import org.dspace.app.rest.model.ScopeEnum; import org.dspace.app.rest.model.SubmissionSectionRest; import org.dspace.app.rest.model.SubmissionVisibilityRest; import org.dspace.app.rest.model.VisibilityEnum; @@ -41,6 +42,7 @@ public SubmissionSectionRest convert(SubmissionStepConfig step, Projection proje sp.setHeader(step.getHeading()); sp.setSectionType(step.getType()); sp.setId(step.getId()); + sp.setScope(ScopeEnum.fromString(step.getScope())); sp.setVisibility(new SubmissionVisibilityRest(VisibilityEnum.fromString(step.getVisibility()), VisibilityEnum.fromString(step.getVisibilityOutside()))); return sp; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/SEOHealthIndicator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/SEOHealthIndicator.java new file mode 100644 index 000000000000..5b57f2d537fc --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/SEOHealthIndicator.java @@ -0,0 +1,100 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.health; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.springframework.boot.actuate.health.AbstractHealthIndicator; +import org.springframework.boot.actuate.health.Health; +import org.springframework.web.client.RestTemplate; + +/** + * Implementation of {@link org.springframework.boot.actuate.health.HealthIndicator} that verifies if the SEO of the + * DSpace instance is configured correctly. + * + * This is only relevant in a production environment, where the DSpace instance is exposed to the public. + */ +public class SEOHealthIndicator extends AbstractHealthIndicator { + + ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + + private final RestTemplate restTemplate = new RestTemplate(); + + @Override + protected void doHealthCheck(Health.Builder builder) { + String baseUrl = configurationService.getProperty("dspace.ui.url"); + + boolean sitemapOk = checkUrl(baseUrl + "/sitemap_index.xml") || checkUrl(baseUrl + "/sitemap_index.html"); + RobotsTxtStatus robotsTxtStatus = checkRobotsTxt(baseUrl + "/robots.txt"); + boolean ssrOk = checkSSR(baseUrl); + + if (sitemapOk && robotsTxtStatus == RobotsTxtStatus.VALID && ssrOk) { + builder.up() + .withDetail("sitemap", "OK") + .withDetail("robots.txt", "OK") + .withDetail("ssr", "OK"); + } else { + builder.down(); + builder.withDetail("sitemap", sitemapOk ? "OK" : "Sitemaps are missing or inaccessible. Please see the " + + "DSpace Documentation on Search Engine Optimization for how to enable Sitemaps."); + + if (robotsTxtStatus == RobotsTxtStatus.MISSING) { + builder.withDetail("robots.txt", "Missing or inaccessible. Please see the DSpace Documentation on " + + "Search Engine Optimization for how to create a robots.txt."); + } else if (robotsTxtStatus == RobotsTxtStatus.INVALID) { + builder.withDetail("robots.txt", "Invalid because it contains localhost URLs. This is often a sign " + + "that a proxy is failing to pass X-Forwarded headers to DSpace. Please see the DSpace " + + "Documentation on Search Engine Optimization for how to pass X-Forwarded headers."); + } else { + builder.withDetail("robots.txt", "OK"); + } + builder.withDetail("ssr", ssrOk ? "OK" : "Server-side rendering (SSR) appears to be disabled. Most " + + "search engines require enabling SSR for proper indexing. Please see the DSpace Documentation on" + + " Search Engine Optimization for more details."); + } + } + + private boolean checkUrl(String url) { + try { + restTemplate.getForEntity(url, String.class); + return true; + } catch (Exception e) { + return false; + } + } + + private RobotsTxtStatus checkRobotsTxt(String url) { + try { + String content = restTemplate.getForObject(url, String.class); + if (StringUtils.isBlank(content)) { + return RobotsTxtStatus.MISSING; + } + if (content.contains("localhost")) { + return RobotsTxtStatus.INVALID; + } + return RobotsTxtStatus.VALID; + } catch (Exception e) { + return RobotsTxtStatus.MISSING; + } + } + + private boolean checkSSR(String url) { + try { + String content = restTemplate.getForObject(url, String.class); + return content != null && !content.contains(""); + } catch (Exception e) { + return false; + } + } + + private enum RobotsTxtStatus { + VALID, MISSING, INVALID + } +} + diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthorizationRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthorizationRest.java index fa463a7c3968..cd3e33b9e2fa 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthorizationRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthorizationRest.java @@ -18,9 +18,9 @@ * @author Andrea Bollini (andrea.bollini at 4science.it) */ @LinksRest(links = { - @LinkRest(method = "getEperson", name = AuthorizationRest.EPERSON), - @LinkRest(method = "getFeature", name = AuthorizationRest.FEATURE), - @LinkRest(method = "getObject", name = AuthorizationRest.OBJECT) + @LinkRest(method = "getEperson", name = AuthorizationRest.EPERSON), + @LinkRest(method = "getFeature", name = AuthorizationRest.FEATURE), + @LinkRest(method = "getObject", name = AuthorizationRest.OBJECT) }) public class AuthorizationRest extends BaseObjectRest { public static final String NAME = "authorization"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/BitstreamRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/BitstreamRest.java index d2c2268b3f35..d456f7222308 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/BitstreamRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/BitstreamRest.java @@ -16,18 +16,9 @@ * @author Andrea Bollini (andrea.bollini at 4science.it) */ @LinksRest(links = { - @LinkRest( - name = BitstreamRest.BUNDLE, - method = "getBundle" - ), - @LinkRest( - name = BitstreamRest.FORMAT, - method = "getFormat" - ), - @LinkRest( - name = BitstreamRest.THUMBNAIL, - method = "getThumbnail" - ) + @LinkRest(name = BitstreamRest.BUNDLE, method = "getBundle"), + @LinkRest(name = BitstreamRest.FORMAT, method = "getFormat"), + @LinkRest(name = BitstreamRest.THUMBNAIL, method = "getThumbnail") }) public class BitstreamRest extends DSpaceObjectRest { public static final String PLURAL_NAME = "bitstreams"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/BrowseIndexRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/BrowseIndexRest.java index a3c0b37ba576..e5b089479971 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/BrowseIndexRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/BrowseIndexRest.java @@ -20,14 +20,8 @@ * @author Andrea Bollini (andrea.bollini at 4science.it) */ @LinksRest(links = { - @LinkRest( - name = BrowseIndexRest.LINK_ITEMS, - method = "listBrowseItems" - ), - @LinkRest( - name = BrowseIndexRest.LINK_ENTRIES, - method = "listBrowseEntries" - ) + @LinkRest(name = BrowseIndexRest.LINK_ITEMS, method = "listBrowseItems"), + @LinkRest(name = BrowseIndexRest.LINK_ENTRIES, method = "listBrowseEntries") }) public class BrowseIndexRest extends BaseObjectRest { private static final long serialVersionUID = -4870333170249999559L; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/BundleRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/BundleRest.java index 1ec9f448dde4..4a417e6c54c3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/BundleRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/BundleRest.java @@ -16,18 +16,9 @@ * @author Jelle Pelgrims (jelle.pelgrims at atmire.com) */ @LinksRest(links = { - @LinkRest( - name = BundleRest.ITEM, - method = "getItem" - ), - @LinkRest( - name = BundleRest.BITSTREAMS, - method = "getBitstreams" - ), - @LinkRest( - name = BundleRest.PRIMARY_BITSTREAM, - method = "getPrimaryBitstream" - ) + @LinkRest(name = BundleRest.ITEM, method = "getItem"), + @LinkRest(name = BundleRest.BITSTREAMS, method = "getBitstreams"), + @LinkRest(name = BundleRest.PRIMARY_BITSTREAM, method = "getPrimaryBitstream") }) public class BundleRest extends DSpaceObjectRest { public static final String NAME = "bundle"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ClaimedTaskRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ClaimedTaskRest.java index 0973fac987d2..d29bf7a7ce6b 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ClaimedTaskRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ClaimedTaskRest.java @@ -16,10 +16,7 @@ * @author Andrea Bollini (andrea.bollini at 4science.it) */ @LinksRest(links = { - @LinkRest( - name = ClaimedTaskRest.STEP, - method = "getStep" - ) + @LinkRest(name = ClaimedTaskRest.STEP, method = "getStep") }) public class ClaimedTaskRest extends BaseObjectRest { public static final String NAME = "claimedtask"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/CollectionRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/CollectionRest.java index f00bb883959c..34faba4cb4d9 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/CollectionRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/CollectionRest.java @@ -15,38 +15,14 @@ * @author Andrea Bollini (andrea.bollini at 4science.it) */ @LinksRest(links = { - @LinkRest( - name = CollectionRest.LICENSE, - method = "getLicense" - ), - @LinkRest( - name = CollectionRest.LOGO, - method = "getLogo" - ), - @LinkRest( - name = CollectionRest.MAPPED_ITEMS, - method = "getMappedItems" - ), - @LinkRest( - name = CollectionRest.PARENT_COMMUNITY, - method = "getParentCommunity" - ), - @LinkRest( - name = CollectionRest.ADMIN_GROUP, - method = "getAdminGroup" - ), - @LinkRest( - name = CollectionRest.SUBMITTERS_GROUP, - method = "getSubmittersGroup" - ), - @LinkRest( - name = CollectionRest.ITEM_READ_GROUP, - method = "getItemReadGroup" - ), - @LinkRest( - name = CollectionRest.BITSTREAM_READ_GROUP, - method = "getBitstreamReadGroup" - ), + @LinkRest(name = CollectionRest.LICENSE, method = "getLicense"), + @LinkRest(name = CollectionRest.LOGO, method = "getLogo"), + @LinkRest(name = CollectionRest.MAPPED_ITEMS, method = "getMappedItems"), + @LinkRest(name = CollectionRest.PARENT_COMMUNITY, method = "getParentCommunity"), + @LinkRest(name = CollectionRest.ADMIN_GROUP, method = "getAdminGroup"), + @LinkRest(name = CollectionRest.SUBMITTERS_GROUP, method = "getSubmittersGroup"), + @LinkRest(name = CollectionRest.ITEM_READ_GROUP, method = "getItemReadGroup"), + @LinkRest(name = CollectionRest.BITSTREAM_READ_GROUP, method = "getBitstreamReadGroup"), }) public class CollectionRest extends DSpaceObjectRest { public static final String NAME = "collection"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/CommunityRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/CommunityRest.java index 0004e0b91ca4..e70b30803da3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/CommunityRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/CommunityRest.java @@ -15,26 +15,11 @@ * @author Andrea Bollini (andrea.bollini at 4science.it) */ @LinksRest(links = { - @LinkRest( - name = CommunityRest.COLLECTIONS, - method = "getCollections" - ), - @LinkRest( - name = CommunityRest.LOGO, - method = "getLogo" - ), - @LinkRest( - name = CommunityRest.SUBCOMMUNITIES, - method = "getSubcommunities" - ), - @LinkRest( - name = CommunityRest.PARENT_COMMUNITY, - method = "getParentCommunity" - ), - @LinkRest( - name = CommunityRest.ADMIN_GROUP, - method = "getAdminGroup" - ) + @LinkRest(name = CommunityRest.COLLECTIONS, method = "getCollections"), + @LinkRest(name = CommunityRest.LOGO, method = "getLogo"), + @LinkRest(name = CommunityRest.SUBCOMMUNITIES, method = "getSubcommunities"), + @LinkRest(name = CommunityRest.PARENT_COMMUNITY, method = "getParentCommunity"), + @LinkRest(name = CommunityRest.ADMIN_GROUP, method = "getAdminGroup") }) public class CommunityRest extends DSpaceObjectRest { public static final String NAME = "community"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/EPersonRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/EPersonRest.java index c06ed0e3fe1f..db243400259d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/EPersonRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/EPersonRest.java @@ -20,10 +20,7 @@ * @author Andrea Bollini (andrea.bollini at 4science.it) */ @LinksRest(links = { - @LinkRest( - name = EPersonRest.GROUPS, - method = "getGroups" - ) + @LinkRest(name = EPersonRest.GROUPS, method = "getGroups") }) public class EPersonRest extends DSpaceObjectRest { public static final String NAME = "eperson"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/EntityTypeRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/EntityTypeRest.java index 9d4a729ded93..e73aa709180d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/EntityTypeRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/EntityTypeRest.java @@ -15,10 +15,7 @@ * Refer to {@link org.dspace.content.EntityType} for explanation of the properties */ @LinksRest(links = { - @LinkRest( - name = EntityTypeRest.RELATION_SHIP_TYPES, - method = "getEntityTypeRelationship" - ) + @LinkRest(name = EntityTypeRest.RELATION_SHIP_TYPES, method = "getEntityTypeRelationship") }) public class EntityTypeRest extends BaseObjectRest { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ExternalSourceRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ExternalSourceRest.java index 58402954e8db..21f41241b293 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ExternalSourceRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ExternalSourceRest.java @@ -13,10 +13,7 @@ * This class serves as a REST representation for an External Source */ @LinksRest(links = { - @LinkRest( - name = ExternalSourceRest.ENTITY_TYPES, - method = "getSupportedEntityTypes" - ) + @LinkRest(name = ExternalSourceRest.ENTITY_TYPES, method = "getSupportedEntityTypes") }) public class ExternalSourceRest extends BaseObjectRest { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/GroupRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/GroupRest.java index 7d56af2e7204..0a4963b66fa0 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/GroupRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/GroupRest.java @@ -18,18 +18,9 @@ */ @JsonIgnoreProperties(ignoreUnknown = true) @LinksRest(links = { - @LinkRest( - name = GroupRest.SUBGROUPS, - method = "getGroups" - ), - @LinkRest( - name = GroupRest.EPERSONS, - method = "getMembers" - ), - @LinkRest( - name = GroupRest.OBJECT, - method = "getParentObject" - ) + @LinkRest(name = GroupRest.SUBGROUPS, method = "getGroups"), + @LinkRest(name = GroupRest.EPERSONS, method = "getMembers"), + @LinkRest(name = GroupRest.OBJECT, method = "getParentObject") }) public class GroupRest extends DSpaceObjectRest { public static final String NAME = "group"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ItemRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ItemRest.java index b2f540c0ac4a..293f95c4de39 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ItemRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ItemRest.java @@ -17,42 +17,15 @@ * @author Andrea Bollini (andrea.bollini at 4science.it) */ @LinksRest(links = { - @LinkRest( - name = ItemRest.ACCESS_STATUS, - method = "getAccessStatus" - ), - @LinkRest( - name = ItemRest.BUNDLES, - method = "getBundles" - ), - @LinkRest( - name = ItemRest.IDENTIFIERS, - method = "getIdentifiers" - ), - @LinkRest( - name = ItemRest.MAPPED_COLLECTIONS, - method = "getMappedCollections" - ), - @LinkRest( - name = ItemRest.OWNING_COLLECTION, - method = "getOwningCollection" - ), - @LinkRest( - name = ItemRest.RELATIONSHIPS, - method = "getRelationships" - ), - @LinkRest( - name = ItemRest.VERSION, - method = "getItemVersion" - ), - @LinkRest( - name = ItemRest.TEMPLATE_ITEM_OF, - method = "getTemplateItemOf" - ), - @LinkRest( - name = ItemRest.THUMBNAIL, - method = "getThumbnail" - ) + @LinkRest(name = ItemRest.ACCESS_STATUS, method = "getAccessStatus"), + @LinkRest(name = ItemRest.BUNDLES, method = "getBundles"), + @LinkRest(name = ItemRest.IDENTIFIERS, method = "getIdentifiers"), + @LinkRest(name = ItemRest.MAPPED_COLLECTIONS, method = "getMappedCollections"), + @LinkRest(name = ItemRest.OWNING_COLLECTION, method = "getOwningCollection"), + @LinkRest(name = ItemRest.RELATIONSHIPS, method = "getRelationships"), + @LinkRest(name = ItemRest.VERSION, method = "getItemVersion"), + @LinkRest(name = ItemRest.TEMPLATE_ITEM_OF, method = "getTemplateItemOf"), + @LinkRest(name = ItemRest.THUMBNAIL, method = "getThumbnail") }) public class ItemRest extends DSpaceObjectRest { public static final String NAME = "item"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/OrcidHistoryRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/OrcidHistoryRest.java index 2c4c7cbe6043..433d5626ca42 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/OrcidHistoryRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/OrcidHistoryRest.java @@ -39,7 +39,7 @@ public class OrcidHistoryRest extends BaseObjectRest { private String responseMessage; - public OrcidHistoryRest(){} + public OrcidHistoryRest() {} @Override @JsonProperty(access = JsonProperty.Access.READ_ONLY) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/PoolTaskRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/PoolTaskRest.java index 0b66f0604b2e..94c70037330e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/PoolTaskRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/PoolTaskRest.java @@ -17,10 +17,7 @@ * @author Andrea Bollini (andrea.bollini at 4science.it) */ @LinksRest(links = { - @LinkRest( - name = PoolTaskRest.STEP, - method = "getStep" - ) + @LinkRest(name = PoolTaskRest.STEP, method = "getStep") }) public class PoolTaskRest extends BaseObjectRest { public static final String NAME = "pooltask"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ProcessRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ProcessRest.java index d3d88c2776ce..fee104b4e389 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ProcessRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ProcessRest.java @@ -21,18 +21,9 @@ * This class serves as a REST representation for the {@link Process} class */ @LinksRest(links = { - @LinkRest( - name = ProcessRest.FILES, - method = "getFilesFromProcess" - ), - @LinkRest( - name = ProcessRest.FILE_TYPES, - method = "getFileTypesFromProcess" - ), - @LinkRest( - name = ProcessRest.OUTPUT, - method = "getOutputFromProcess" - ) + @LinkRest(name = ProcessRest.FILES, method = "getFilesFromProcess"), + @LinkRest(name = ProcessRest.FILE_TYPES, method = "getFileTypesFromProcess"), + @LinkRest(name = ProcessRest.OUTPUT, method = "getOutputFromProcess") }) public class ProcessRest extends BaseObjectRest { public static final String NAME = "process"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RelationshipRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RelationshipRest.java index 76a7a4348682..723f7e148b27 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RelationshipRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RelationshipRest.java @@ -19,10 +19,7 @@ * Refer to {@link org.dspace.content.Relationship} for explanation about the properties */ @LinksRest(links = { - @LinkRest( - name = RelationshipRest.RELATIONSHIP_TYPE, - method = "getRelationshipType" - ) + @LinkRest(name = RelationshipRest.RELATIONSHIP_TYPE, method = "getRelationshipType") }) public class RelationshipRest extends BaseObjectRest { public static final String NAME = "relationship"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResearcherProfileRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResearcherProfileRest.java index 13faa2e2bbdf..629dbdf85821 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResearcherProfileRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResearcherProfileRest.java @@ -20,8 +20,8 @@ * */ @LinksRest(links = { - @LinkRest(name = ResearcherProfileRest.ITEM, method = "getItem"), - @LinkRest(name = ResearcherProfileRest.EPERSON, method = "getEPerson") + @LinkRest(name = ResearcherProfileRest.ITEM, method = "getItem"), + @LinkRest(name = ResearcherProfileRest.EPERSON, method = "getEPerson") }) public class ResearcherProfileRest extends BaseObjectRest { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SuggestionRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SuggestionRest.java index c7210e892558..7b1a05127fc7 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SuggestionRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SuggestionRest.java @@ -21,7 +21,9 @@ * * @author Andrea Bollini (andrea.bollini at 4science.it) */ -@LinksRest(links = { @LinkRest(name = SuggestionRest.TARGET, method = "getTarget") }) +@LinksRest(links = { + @LinkRest(name = SuggestionRest.TARGET, method = "getTarget") +}) public class SuggestionRest extends BaseObjectRest { private static final long serialVersionUID = 1L; public static final String NAME = "suggestion"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SuggestionTargetRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SuggestionTargetRest.java index 65764507e247..b6518eff7488 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SuggestionTargetRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SuggestionTargetRest.java @@ -19,7 +19,7 @@ * @author Andrea Bollini (andrea.bollini at 4science.it) */ @LinksRest(links = { - @LinkRest(name = SuggestionTargetRest.TARGET, method = "getTarget") + @LinkRest(name = SuggestionTargetRest.TARGET, method = "getTarget") }) public class SuggestionTargetRest extends BaseObjectRest { private static final long serialVersionUID = 1L; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VersionHistoryRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VersionHistoryRest.java index 5aab7028a8c6..80f704c77936 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VersionHistoryRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VersionHistoryRest.java @@ -13,14 +13,8 @@ * The REST object for the {@link org.dspace.versioning.VersionHistory} object */ @LinksRest(links = { - @LinkRest( - name = VersionHistoryRest.VERSIONS, - method = "getVersions" - ), - @LinkRest( - name = VersionHistoryRest.DRAFT_VERSION, - method = "getDraftVersion" - ) + @LinkRest(name = VersionHistoryRest.VERSIONS, method = "getVersions"), + @LinkRest(name = VersionHistoryRest.DRAFT_VERSION, method = "getDraftVersion") }) public class VersionHistoryRest extends BaseObjectRest { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VersionRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VersionRest.java index 21bf82804dd2..d9ebdd67e408 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VersionRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VersionRest.java @@ -16,14 +16,8 @@ * The REST object for the {@link org.dspace.versioning.Version} objects */ @LinksRest(links = { - @LinkRest( - name = VersionRest.VERSION_HISTORY, - method = "getVersionHistory" - ), - @LinkRest( - name = VersionRest.ITEM, - method = "getVersionItem" - ) + @LinkRest(name = VersionRest.VERSION_HISTORY, method = "getVersionHistory"), + @LinkRest(name = VersionRest.ITEM, method = "getVersionItem") }) public class VersionRest extends BaseObjectRest { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VocabularyEntryDetailsRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VocabularyEntryDetailsRest.java index e5869a852521..884e14642cf9 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VocabularyEntryDetailsRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VocabularyEntryDetailsRest.java @@ -18,9 +18,9 @@ * @author Andrea Bollini (andrea.bollini at 4science.it) */ @LinksRest(links = { - @LinkRest(name = VocabularyEntryDetailsRest.PARENT, method = "getParent"), - @LinkRest(name = VocabularyEntryDetailsRest.CHILDREN, method = "getChildren") - }) + @LinkRest(name = VocabularyEntryDetailsRest.PARENT, method = "getParent"), + @LinkRest(name = VocabularyEntryDetailsRest.CHILDREN, method = "getChildren") +}) public class VocabularyEntryDetailsRest extends BaseObjectRest { public static final String PLURAL_NAME = "vocabularyEntryDetails"; public static final String NAME = "vocabularyEntryDetail"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VocabularyRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VocabularyRest.java index f119059c2bb7..a54d93c643b4 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VocabularyRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VocabularyRest.java @@ -15,9 +15,7 @@ * @author Andrea Bollini (andrea.bollini at 4science.it) */ @LinksRest(links = { - @LinkRest(name = VocabularyRest.ENTRIES, - method = "filter" - ), + @LinkRest(name = VocabularyRest.ENTRIES, method = "filter"), }) public class VocabularyRest extends BaseObjectRest { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkflowDefinitionRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkflowDefinitionRest.java index 0ec967d09876..9cef79aaf3be 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkflowDefinitionRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkflowDefinitionRest.java @@ -18,14 +18,8 @@ * @author Maria Verdonck (Atmire) on 11/12/2019 */ @LinksRest(links = { - @LinkRest( - name = WorkflowDefinitionRest.COLLECTIONS_MAPPED_TO, - method = "getCollections" - ), - @LinkRest( - name = WorkflowDefinitionRest.STEPS, - method = "getSteps" - ) + @LinkRest(name = WorkflowDefinitionRest.COLLECTIONS_MAPPED_TO, method = "getCollections"), + @LinkRest(name = WorkflowDefinitionRest.STEPS, method = "getSteps") }) public class WorkflowDefinitionRest extends BaseObjectRest { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkflowItemRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkflowItemRest.java index 65fa531c5e42..d08abb3546a3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkflowItemRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkflowItemRest.java @@ -15,22 +15,10 @@ * @author Andrea Bollini (andrea.bollini at 4science.it) */ @LinksRest(links = { - @LinkRest( - name = WorkflowItemRest.STEP, - method = "getStep" - ), - @LinkRest( - name = WorkflowItemRest.SUBMITTER, - method = "getWorkflowItemSubmitter" - ), - @LinkRest( - name = WorkflowItemRest.ITEM, - method = "getWorkflowItemItem" - ), - @LinkRest( - name = WorkflowItemRest.COLLECTION, - method = "getWorkflowItemCollection" - ) + @LinkRest(name = WorkflowItemRest.STEP, method = "getStep"), + @LinkRest(name = WorkflowItemRest.SUBMITTER, method = "getWorkflowItemSubmitter"), + @LinkRest(name = WorkflowItemRest.ITEM, method = "getWorkflowItemItem"), + @LinkRest(name = WorkflowItemRest.COLLECTION, method = "getWorkflowItemCollection") }) public class WorkflowItemRest extends AInprogressSubmissionRest { public static final String NAME = "workflowitem"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkflowStepRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkflowStepRest.java index b3397721c117..53ddf38709e4 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkflowStepRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkflowStepRest.java @@ -18,10 +18,7 @@ * @author Maria Verdonck (Atmire) on 10/01/2020 */ @LinksRest(links = { - @LinkRest( - name = WorkflowStepRest.ACTIONS, - method = "getActions" - ), + @LinkRest(name = WorkflowStepRest.ACTIONS, method = "getActions"), }) public class WorkflowStepRest extends BaseObjectRest { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkspaceItemRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkspaceItemRest.java index e311cd259231..8e0d52123f99 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkspaceItemRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkspaceItemRest.java @@ -15,22 +15,10 @@ * @author Andrea Bollini (andrea.bollini at 4science.it) */ @LinksRest(links = { - @LinkRest( - name = WorkspaceItemRest.SUPERVISION_ORDERS, - method = "getSupervisionOrders" - ), - @LinkRest( - name = WorkspaceItemRest.SUBMITTER, - method = "getWorkspaceItemSubmitter" - ), - @LinkRest( - name = WorkspaceItemRest.ITEM, - method = "getWorkspaceItemItem" - ), - @LinkRest( - name = WorkspaceItemRest.COLLECTION, - method = "getWorkspaceItemCollection" - ) + @LinkRest(name = WorkspaceItemRest.SUPERVISION_ORDERS, method = "getSupervisionOrders"), + @LinkRest(name = WorkspaceItemRest.SUBMITTER, method = "getWorkspaceItemSubmitter"), + @LinkRest(name = WorkspaceItemRest.ITEM, method = "getWorkspaceItemItem"), + @LinkRest(name = WorkspaceItemRest.COLLECTION, method = "getWorkspaceItemCollection") }) public class WorkspaceItemRest extends AInprogressSubmissionRest { public static final String NAME = "workspaceitem"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java index 74beeb3dac84..ff8891eb0859 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.databind.JsonNode; import jakarta.servlet.http.HttpServletRequest; -import org.apache.logging.log4j.Logger; import org.dspace.app.rest.exception.DSpaceBadRequestException; import org.dspace.app.rest.exception.RESTAuthorizationException; import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException; @@ -26,7 +25,6 @@ import org.dspace.app.rest.model.RestAddressableModel; import org.dspace.app.rest.model.patch.Patch; import org.dspace.authorize.AuthorizeException; -import org.dspace.content.service.MetadataFieldService; import org.dspace.core.Context; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.annotation.Autowired; @@ -50,17 +48,12 @@ public abstract class DSpaceRestRepository, PagingAndSortingRepository, BeanNameAware { - private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(DSpaceRestRepository.class); - private String thisRepositoryBeanName; private DSpaceRestRepository thisRepository; @Autowired private ApplicationContext applicationContext; - @Autowired - private MetadataFieldService metadataFieldService; - /** * From BeanNameAware. Allows us to capture the name of the bean, so we can load it into thisRepository. * See getThisRepository() method. diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ProcessRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ProcessRestRepository.java index 24152ed5697a..6ec7c62aa679 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ProcessRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ProcessRestRepository.java @@ -14,6 +14,7 @@ import java.util.UUID; import java.util.stream.Collectors; +import jakarta.annotation.PostConstruct; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -65,6 +66,12 @@ public class ProcessRestRepository extends DSpaceRestRepository getDomainClass() { * Generate a link back to DSpace, to act on a request. * * @param token identifies the request. - * @return URL to the item request API, with the token as request parameter - * "token". + * @return URL to the item request API, with /request-a-copy/{token} as the last URL segments * @throws URISyntaxException passed through. * @throws MalformedURLException passed through. */ - private String getLinkTokenEmail(String token) + public String getLinkTokenEmail(String token) throws URISyntaxException, MalformedURLException { final String base = configurationService.getProperty("dspace.ui.url"); - URI link = new URIBuilder(base) - .setPathSegments("request-a-copy", token) - .build(); + // Construct the link, making sure to support sub-paths + URIBuilder uriBuilder = new URIBuilder(base); + List segments = new LinkedList<>(); + if (StringUtils.isNotBlank(uriBuilder.getPath())) { + segments.add(StringUtils.strip(uriBuilder.getPath(), "/")); + } + segments.add("request-a-copy"); + segments.add(token); - return link.toURL().toExternalForm(); + // Build and return the URL from segments (or throw exception) + return uriBuilder.setPathSegments(segments).build().toURL().toExternalForm(); } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResourcePolicyRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResourcePolicyRestRepository.java index 8b598aeeae32..1bfc50481078 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResourcePolicyRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResourcePolicyRestRepository.java @@ -25,6 +25,7 @@ import org.dspace.app.rest.model.ResourcePolicyRest; import org.dspace.app.rest.model.patch.Patch; import org.dspace.app.rest.repository.patch.ResourcePatch; +import org.dspace.app.rest.security.DSpacePermissionEvaluator; import org.dspace.app.rest.utils.DSpaceObjectUtils; import org.dspace.app.rest.utils.Utils; import org.dspace.authorize.AuthorizeException; @@ -44,6 +45,8 @@ import org.springframework.data.rest.webmvc.ResourceNotFoundException; import org.springframework.hateoas.Link; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; /** @@ -73,6 +76,9 @@ public class ResourcePolicyRestRepository extends DSpaceRestRepository resourcePatch; + @Autowired + private DSpacePermissionEvaluator permissionEvaluator; + @Autowired DiscoverableEndpointsService discoverableEndpointsService; @@ -222,14 +228,13 @@ public Page findByGroup(@Parameter(value = "uuid", required } @Override - @PreAuthorize("hasAuthority('ADMIN')") + @PreAuthorize("isAuthenticated()") protected ResourcePolicyRest createAndReturn(Context context) throws AuthorizeException, SQLException { String resourceUuidStr = getRequestService().getCurrentRequest().getServletRequest().getParameter("resource"); String epersonUuidStr = getRequestService().getCurrentRequest().getServletRequest().getParameter("eperson"); String groupUuidStr = getRequestService().getCurrentRequest().getServletRequest().getParameter("group"); - if (resourceUuidStr == null) { throw new MissingParameterException("Missing resource (uuid) parameter"); } @@ -244,6 +249,11 @@ protected ResourcePolicyRest createAndReturn(Context context) throws AuthorizeEx UUID resourceUuid = UUID.fromString(resourceUuidStr); + if (isNotAuthorized(resourceUuid, "WRITE")) { + throw new AuthorizeException( + "User unauthorized to create a new ResourcePolicy for resource: " + resourceUuid); + } + try { resourcePolicyRest = mapper.readValue(req.getInputStream(), ResourcePolicyRest.class); } catch (IOException exIO) { @@ -298,7 +308,7 @@ protected ResourcePolicyRest createAndReturn(Context context) throws AuthorizeEx } @Override - @PreAuthorize("hasAuthority('ADMIN')") + @PreAuthorize("hasPermission(#id, 'resourcepolicy', 'ADMIN')") protected void delete(Context context, Integer id) throws AuthorizeException { ResourcePolicy resourcePolicy = null; try { @@ -332,4 +342,10 @@ public void afterPropertiesSet() throws Exception { Link.of("/api/" + ResourcePolicyRest.CATEGORY + "/" + ResourcePolicyRest.PLURAL_NAME + "/search", ResourcePolicyRest.PLURAL_NAME + "-search"))); } + + private boolean isNotAuthorized(UUID id, String permission) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return !permissionEvaluator.hasPermission(authentication, id, "resourcepolicy", permission); + } + } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RootRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RootRestRepository.java index 54c678c18e9c..3600ed19e1b2 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RootRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RootRestRepository.java @@ -7,6 +7,7 @@ */ package org.dspace.app.rest.repository; +import jakarta.servlet.http.HttpServletRequest; import org.dspace.app.rest.converter.RootConverter; import org.dspace.app.rest.model.RootRest; import org.springframework.beans.factory.annotation.Autowired; @@ -21,7 +22,7 @@ public class RootRestRepository { @Autowired RootConverter rootConverter; - public RootRest getRoot() { - return rootConverter.convert(); + public RootRest getRoot(HttpServletRequest request) { + return rootConverter.convert(request); } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemRestRepository.java index f6c879b3efdf..3d852e9aabf0 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemRestRepository.java @@ -71,6 +71,7 @@ public class WorkflowItemRestRepository extends DSpaceRestRepository { public static final String OPERATION_PATH_SECTIONS = "sections"; + public static final String REQUESTPARAMETER_EXPUNGE = "expunge"; private static final Logger log = LogManager.getLogger(); @@ -239,13 +240,25 @@ public void patch(Context context, HttpServletRequest request, String apiCategor * move the workflowitem back to the submitter workspace regardless to how the workflow is designed */ protected void delete(Context context, Integer id) { + String expungeParam = getRequestService() + .getCurrentRequest() + .getServletRequest() + .getParameter(REQUESTPARAMETER_EXPUNGE); + boolean expunge = false; + if (expungeParam != null) { + expunge = Boolean.parseBoolean(expungeParam); + } XmlWorkflowItem witem = null; try { witem = wis.find(context, id); if (witem == null) { throw new ResourceNotFoundException("WorkflowItem ID " + id + " not found"); } - wfs.abort(context, witem, context.getCurrentUser()); + if (expunge) { + wis.delete(context, witem); + } else { + wfs.abort(context, witem, context.getCurrentUser()); + } } catch (AuthorizeException e) { throw new RESTAuthorizationException(e); } catch (SQLException e) { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataPatchUtils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataPatchUtils.java index 954cc844f237..5bbd5c013c9e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataPatchUtils.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataPatchUtils.java @@ -14,6 +14,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; import org.dspace.app.rest.model.MetadataValueRest; import org.dspace.app.rest.model.patch.JsonValueEvaluator; import org.dspace.app.rest.model.patch.Operation; @@ -135,10 +136,20 @@ protected String extractNewValueOfMd(Operation operation) { * @param context Context the retrieve metadataField from service with string * @param operation Operation of the patch * @return The metadataField corresponding to the md element string of the operation + * Null if no metadata field is passed in the operation + * @throws UnprocessableEntityException if an invalid metadata field is passed in the operation */ - protected MetadataField getMetadataField(Context context, Operation operation) throws SQLException { + protected MetadataField getMetadataField(Context context, Operation operation) + throws SQLException, UnprocessableEntityException { String mdElement = this.extractMdFieldStringFromOperation(operation); - return metadataFieldService.findByString(context, mdElement, '.'); + if (StringUtils.isBlank(mdElement)) { + return null; + } + MetadataField metadataField = metadataFieldService.findByString(context, mdElement, '.'); + if (metadataField == null) { + throw new UnprocessableEntityException("Metadata field does not exist"); + } + return metadataField; } /** diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/scripts/handler/impl/RestDSpaceRunnableHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/scripts/handler/impl/RestDSpaceRunnableHandler.java index 596ab4429093..ee67baa8ab38 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/scripts/handler/impl/RestDSpaceRunnableHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/scripts/handler/impl/RestDSpaceRunnableHandler.java @@ -130,7 +130,7 @@ public void handleCompletion() { @Override public void handleException(Exception e) { - handleException(null, e); + handleException(e.getMessage(), e); } @Override diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/CustomLogoutHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/CustomLogoutHandler.java index d94e7c82ccbd..8c0b4194a798 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/CustomLogoutHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/CustomLogoutHandler.java @@ -45,7 +45,7 @@ public void logout(HttpServletRequest httpServletRequest, HttpServletResponse ht try { Context context = ContextUtil.obtainContext(httpServletRequest); restAuthenticationService.invalidateAuthenticationData(httpServletRequest, httpServletResponse, context); - context.commit(); + context.complete(); } catch (Exception e) { log.error("Unable to logout", e); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestAuthenticationProvider.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestAuthenticationProvider.java index d66ecd547274..38685f111f79 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestAuthenticationProvider.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestAuthenticationProvider.java @@ -85,7 +85,7 @@ public Authentication authenticate(Authentication authentication) throws Authent } else { // Otherwise, this is a new login & we need to attempt authentication log.debug("Request to authenticate new login"); - return authenticateNewLogin(authentication); + return authenticateNewLogin(context, authentication); } } @@ -107,55 +107,43 @@ private Authentication authenticateRefreshTokenRequest(Context context) { * If login is successful, returns a NEW Authentication class containing the logged in EPerson and their list of * GrantedAuthority objects. If login fails, a BadCredentialsException is thrown. If no valid login found implicit * or explicit, then null is returned. + * + * @param context The current DSpace context * @param authentication Authentication class to attempt authentication. * @return new Authentication class containing logged-in user information or null */ - private Authentication authenticateNewLogin(Authentication authentication) { - Context newContext = null; + private Authentication authenticateNewLogin(Context context, Authentication authentication) { Authentication output = null; if (authentication != null) { - try { - newContext = new Context(); - String name = authentication.getName(); - String password = Objects.toString(authentication.getCredentials(), null); + String name = authentication.getName(); + String password = Objects.toString(authentication.getCredentials(), null); - int implicitStatus = authenticationService.authenticateImplicit(newContext, null, null, null, request); + int implicitStatus = authenticationService.authenticateImplicit(context, null, null, null, request); - if (implicitStatus == AuthenticationMethod.SUCCESS) { - log.info(LogHelper.getHeader(newContext, "login", "type=implicit")); - output = createAuthentication(newContext); - } else { - int authenticateResult = authenticationService - .authenticate(newContext, name, password, null, request); - if (AuthenticationMethod.SUCCESS == authenticateResult) { + if (implicitStatus == AuthenticationMethod.SUCCESS) { + log.info(LogHelper.getHeader(context, "login", "type=implicit")); + output = createAuthentication(context); + } else { + int authenticateResult = authenticationService.authenticate(context, name, password, null, request); + if (AuthenticationMethod.SUCCESS == authenticateResult) { - log.info(LogHelper - .getHeader(newContext, "login", "type=explicit")); + log.info(LogHelper.getHeader(context, "login", "type=explicit")); - output = createAuthentication(newContext); + output = createAuthentication(context); - for (PostLoggedInAction action : postLoggedInActions) { - try { - action.loggedIn(newContext); - } catch (Exception ex) { - log.error("An error occurs performing post logged in action", ex); - } + for (PostLoggedInAction action : postLoggedInActions) { + try { + action.loggedIn(context); + } catch (Exception ex) { + log.error("An error occurs performing post logged in action", ex); } - - } else { - log.info(LogHelper.getHeader(newContext, "failed_login", - "email={}, result={}"), name, authenticateResult); - throw new BadCredentialsException("Login failed"); - } - } - } finally { - if (newContext != null && newContext.isValid()) { - try { - newContext.complete(); - } catch (SQLException e) { - log.error("{} occurred while trying to close", e.getMessage(), e); } + + } else { + log.info(LogHelper.getHeader(context, "failed_login", + "email={}, result={}"), name, authenticateResult); + throw new BadCredentialsException("Login failed"); } } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OidcLoginFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OidcLoginFilter.java index 28c67314c5ca..6b5b25da6c9d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OidcLoginFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OidcLoginFilter.java @@ -10,11 +10,18 @@ import static org.dspace.authenticate.OidcAuthenticationBean.OIDC_AUTH_ATTRIBUTE; import java.io.IOException; +import java.util.ArrayList; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; 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.core.Utils; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -28,6 +35,11 @@ */ public class OidcLoginFilter extends StatelessLoginFilter { + private static final Logger log = LogManager.getLogger(OidcLoginFilter.class); + + private final ConfigurationService configurationService = DSpaceServicesFactory.getInstance() + .getConfigurationService(); + public OidcLoginFilter(String url, String httpMethod, AuthenticationManager authenticationManager, RestAuthenticationService restAuthenticationService) { super(url, httpMethod, authenticationManager, restAuthenticationService); @@ -45,7 +57,45 @@ public Authentication attemptAuthentication(HttpServletRequest req, HttpServletR protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException { restAuthenticationService.addAuthenticationDataForUser(req, res, (DSpaceAuthentication) auth, true); - chain.doFilter(req, res); + redirectAfterSuccess(req, res); + } + + /** + * After successful login, redirect to the DSpace URL specified by this OIDC + * request (in the "redirectUrl" request parameter). If that 'redirectUrl' is + * not valid or trusted for this DSpace site, then return a 400 error. + * @param request + * @param response + * @throws IOException + */ + private void redirectAfterSuccess(HttpServletRequest request, HttpServletResponse response) throws IOException { + // Get redirect URL from request parameter + String redirectUrl = request.getParameter("redirectUrl"); + + // If redirectUrl unspecified, default to the configured UI + if (StringUtils.isEmpty(redirectUrl)) { + redirectUrl = configurationService.getProperty("dspace.ui.url"); + } + + // Validate that the redirectURL matches either the server or UI hostname. It + // *cannot* be an arbitrary URL. + String redirectHostName = Utils.getHostName(redirectUrl); + String serverHostName = Utils.getHostName(configurationService.getProperty("dspace.server.url")); + ArrayList allowedHostNames = new ArrayList<>(); + allowedHostNames.add(serverHostName); + String[] allowedUrls = configurationService.getArrayProperty("rest.cors.allowed-origins"); + for (String url : allowedUrls) { + allowedHostNames.add(Utils.getHostName(url)); + } + + if (StringUtils.equalsAnyIgnoreCase(redirectHostName, allowedHostNames.toArray(new String[0]))) { + log.debug("OIDC redirecting to " + redirectUrl); + response.sendRedirect(redirectUrl); + } else { + log.error("Invalid OIDC redirectURL=" + redirectUrl + ". URL doesn't match hostname of server or UI!"); + response.sendError(HttpServletResponse.SC_BAD_REQUEST, + "Invalid redirectURL! Must match server or ui hostname."); + } } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyAdminPermissionEvalutatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyAdminPermissionEvalutatorPlugin.java index ccf272ecefae..ad83284b84b3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyAdminPermissionEvalutatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyAdminPermissionEvalutatorPlugin.java @@ -9,12 +9,15 @@ import java.io.Serializable; import java.sql.SQLException; +import java.util.UUID; +import org.apache.commons.lang.math.NumberUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.ResourcePolicyRest; import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.app.util.factory.UtilServiceFactory; import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.AuthorizeService; import org.dspace.authorize.service.ResourcePolicyService; @@ -38,7 +41,7 @@ public class ResourcePolicyAdminPermissionEvalutatorPlugin extends RestObjectPer private static final Logger log = LogManager.getLogger(); - public static final String RESOURCE_POLICY_PATCH = "resourcepolicy"; + public static final String RESOURCE_POLICY_TYPE = "resourcepolicy"; @Autowired AuthorizeService authorizeService; @@ -55,8 +58,9 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t DSpaceRestPermission restPermission = DSpaceRestPermission.convert(permission); - if (!DSpaceRestPermission.ADMIN.equals(restPermission) - || !StringUtils.equalsIgnoreCase(targetType, RESOURCE_POLICY_PATCH)) { + if (!DSpaceRestPermission.ADMIN.equals(restPermission) && + !DSpaceRestPermission.WRITE.equals(restPermission) || + !StringUtils.equalsIgnoreCase(targetType, RESOURCE_POLICY_TYPE)) { return false; } @@ -64,19 +68,37 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t Context context = ContextUtil.obtainContext(request.getHttpServletRequest()); try { - int resourcePolicyID = Integer.parseInt(targetId.toString()); - ResourcePolicy resourcePolicy = resourcePolicyService.find(context, resourcePolicyID); - if (resourcePolicy == null) { - throw new ResourceNotFoundException( - ResourcePolicyRest.CATEGORY + "." + ResourcePolicyRest.NAME + - " with id: " + resourcePolicyID + " not found"); + DSpaceObject dso = null; + if (NumberUtils.isNumber(targetId.toString())) { + var id = Integer.parseInt(targetId.toString()); + dso = getDSO(context, id); + } else { + var uuid = UUID.fromString(targetId.toString()); + dso = getDSO(context, uuid); } - DSpaceObject dso = resourcePolicy.getdSpaceObject(); return authorizeService.isAdmin(context, dso); + } catch (SQLException e) { log.error(e::getMessage, e); } return false; } + private DSpaceObject getDSO(Context context, int id) throws SQLException { + ResourcePolicy resourcePolicy = resourcePolicyService.find(context, id); + if (resourcePolicy == null) { + throw new ResourceNotFoundException( + ResourcePolicyRest.CATEGORY + "." + ResourcePolicyRest.NAME + " with id: " + id + " not found"); + } + return resourcePolicy.getdSpaceObject(); + } + + private DSpaceObject getDSO(Context context, UUID uuid) throws SQLException { + DSpaceObject dso = UtilServiceFactory.getInstance().getDSpaceObjectUtils().findDSpaceObject(context, uuid); + if (dso == null) { + throw new ResourceNotFoundException("DSpaceObject with uuid: " + uuid + " not found"); + } + return dso; + } + } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyRestPermissionEvaluatorPlugin.java index 9a34ca68110d..6635a87a229a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyRestPermissionEvaluatorPlugin.java @@ -55,7 +55,6 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t DSpaceRestPermission restPermission = DSpaceRestPermission.convert(permission); if (!DSpaceRestPermission.READ.equals(restPermission) - && !DSpaceRestPermission.WRITE.equals(restPermission) && !DSpaceRestPermission.DELETE.equals(restPermission) || !StringUtils.equalsIgnoreCase(targetType, ResourcePolicyRest.NAME)) { return false; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java index a2928fc96fb1..0a6307b406cd 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java @@ -83,6 +83,9 @@ public void addAuthenticationDataForUser(HttpServletRequest request, HttpServlet String token = loginJWTTokenHandler.createTokenForEPerson(context, request, authentication.getPreviousLoginDate()); context.commit(); + // Close the Context, because the DSpaceRequestContextFilter is not called for requests that trigger + // the authentication filters (filters that extend AbstractAuthenticationProcessingFilter) + context.close(); // Add newly generated auth token to the response addTokenToResponse(request, response, token, addCookie); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/controller/LinksetRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/controller/LinksetRestController.java index b1731fb10bb7..8bf7151ec663 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/controller/LinksetRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/controller/LinksetRestController.java @@ -144,7 +144,7 @@ public List getHeader(HttpServletRequest request, @PathVariable U DSpaceObject dso = findObject(context, uuid); List linksetNodes = linksetService.createLinksetNodesForSingleLinkset(request, context, dso); return linksetNodes.stream() - .map(node -> new TypedLinkRest(node.getLink(), node.getRelation(), node.getType())) + .map(node -> new TypedLinkRest(node.getLink(), node.getRelation(), node.getType(), node.getProfile())) .collect(Collectors.toList()); } @@ -163,7 +163,8 @@ public String getDescribedBy( DSpaceObject object = findObject(context, uuid); DisseminationCrosswalk xwalk = (DisseminationCrosswalk) pluginService.getNamedPlugin(DisseminationCrosswalk.class, xwalkName); - List elements = xwalk.disseminateList(context, object); + // Output valid XML: disseminate using root element as opposed to list + Element elements = xwalk.disseminateElement(context, object); XMLOutputter outputter = new XMLOutputter(Format.getCompactFormat()); return outputter.outputString(elements); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/converter/LinksetRestMessageConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/converter/LinksetRestMessageConverter.java index 24c8e6735dc9..6ecfc3aa3a48 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/converter/LinksetRestMessageConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/converter/LinksetRestMessageConverter.java @@ -44,8 +44,16 @@ public static String convert(List> linksetNodes) { if (isNotBlank(linksetNode.getAnchor())) { responseBody.append(format("; anchor=\"%s\" ", linksetNode.getAnchor())); } + if (isNotBlank(linksetNode.getProfile())) { + responseBody.append(format("; profile=\"%s\" ", linksetNode.getProfile())); + } responseBody.append(", "); }); + + if (responseBody.length() >= 2 && responseBody.substring(responseBody.length() - 2).equals(", ")) { + responseBody.delete(responseBody.length() - 2, responseBody.length()); + } + return responseBody.toString(); } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/model/LinksetNode.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/model/LinksetNode.java index 8c7347350faa..cf71739a3749 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/model/LinksetNode.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/model/LinksetNode.java @@ -22,6 +22,12 @@ public class LinksetNode { private String type; @JsonInclude(JsonInclude.Include.NON_NULL) private String anchor; + private String profile; + + public LinksetNode(String link, LinksetRelationType relation, String type, String profile, String anchor) { + this(link, relation, type, anchor); + this.profile = profile; + } public LinksetNode(String link, LinksetRelationType relation, String type, String anchor) { this(link, relation, anchor); @@ -65,4 +71,12 @@ public String getAnchor() { public void setAnchor(String anchor) { this.anchor = anchor; } + + public String getProfile() { + return profile; + } + + public void setProfile(String profile) { + this.profile = profile; + } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/model/TypedLinkRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/model/TypedLinkRest.java index ce6668920a9c..e89505bbbb63 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/model/TypedLinkRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/model/TypedLinkRest.java @@ -28,6 +28,8 @@ public class TypedLinkRest extends RestAddressableModel { private String type; + private String profile; + public TypedLinkRest() { } @@ -37,6 +39,11 @@ public TypedLinkRest(String href, LinksetRelationType rel, String type) { this.type = type; } + public TypedLinkRest(String href, LinksetRelationType rel, String type, String profile) { + this(href, rel, type); + this.profile = profile; + } + public String getHref() { return href; } @@ -62,6 +69,14 @@ public String getType() { return type; } + public void setProfile(String profile) { + this.profile = profile; + } + + public String getProfile() { + return profile; + } + @Override public String getTypePlural() { return PLURAL_NAME; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/bitstream/BitstreamLinksetProcessor.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/bitstream/BitstreamLinksetProcessor.java index 97eb9f2a546d..349dde7b6dac 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/bitstream/BitstreamLinksetProcessor.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/bitstream/BitstreamLinksetProcessor.java @@ -10,7 +10,8 @@ import java.util.List; import jakarta.servlet.http.HttpServletRequest; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.signposting.model.LinksetNode; import org.dspace.app.rest.signposting.model.LinksetRelationType; import org.dspace.content.Bitstream; @@ -25,7 +26,7 @@ */ public class BitstreamLinksetProcessor extends BitstreamSignpostingProcessor { - private static final Logger log = Logger.getLogger(BitstreamLinksetProcessor.class); + private static final Logger log = LogManager.getLogger(BitstreamLinksetProcessor.class); private final BitstreamService bitstreamService; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/bitstream/BitstreamParentItemProcessor.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/bitstream/BitstreamParentItemProcessor.java index 32928dfa8892..c855f06784f7 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/bitstream/BitstreamParentItemProcessor.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/bitstream/BitstreamParentItemProcessor.java @@ -10,7 +10,8 @@ import java.util.List; import jakarta.servlet.http.HttpServletRequest; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.signposting.model.LinksetNode; import org.dspace.app.rest.signposting.model.LinksetRelationType; import org.dspace.content.Bitstream; @@ -28,7 +29,7 @@ */ public class BitstreamParentItemProcessor extends BitstreamSignpostingProcessor { - private static final Logger log = Logger.getLogger(BitstreamParentItemProcessor.class); + private static final Logger log = LogManager.getLogger(BitstreamParentItemProcessor.class); private final BitstreamService bitstreamService; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/bitstream/BitstreamTypeProcessor.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/bitstream/BitstreamTypeProcessor.java index 8889a415d327..d0f170b4c55a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/bitstream/BitstreamTypeProcessor.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/bitstream/BitstreamTypeProcessor.java @@ -11,7 +11,8 @@ import jakarta.servlet.http.HttpServletRequest; import org.apache.commons.lang3.StringUtils; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.signposting.model.LinksetNode; import org.dspace.app.rest.signposting.model.LinksetRelationType; import org.dspace.content.Bitstream; @@ -28,7 +29,7 @@ */ public class BitstreamTypeProcessor extends BitstreamSignpostingProcessor { - private static final Logger log = Logger.getLogger(BitstreamTypeProcessor.class); + private static final Logger log = LogManager.getLogger(BitstreamTypeProcessor.class); @Autowired private SimpleMapConverter mapConverterDSpaceToSchemaOrgUri; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemAuthorProcessor.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemAuthorProcessor.java index f3a9e35198a7..ebc7c46f4d23 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemAuthorProcessor.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemAuthorProcessor.java @@ -16,7 +16,8 @@ import java.util.List; import jakarta.servlet.http.HttpServletRequest; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.signposting.model.LinksetNode; import org.dspace.app.rest.signposting.model.LinksetRelationType; import org.dspace.content.Item; @@ -37,7 +38,7 @@ public class ItemAuthorProcessor extends ItemSignpostingProcessor { /** * log4j category */ - private static final Logger log = Logger.getLogger(ItemAuthorProcessor.class); + private static final Logger log = LogManager.getLogger(ItemAuthorProcessor.class); private final ItemService itemService; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemContentBitstreamsProcessor.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemContentBitstreamsProcessor.java index 0b91e57f7b3f..65a29c2b6c98 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemContentBitstreamsProcessor.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemContentBitstreamsProcessor.java @@ -11,7 +11,8 @@ import java.util.List; import jakarta.servlet.http.HttpServletRequest; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.signposting.model.LinksetNode; import org.dspace.app.rest.signposting.model.LinksetRelationType; import org.dspace.content.Bitstream; @@ -33,7 +34,7 @@ public class ItemContentBitstreamsProcessor extends ItemSignpostingProcessor { /** * log4j category */ - private static final Logger log = Logger.getLogger(ItemContentBitstreamsProcessor.class); + private static final Logger log = LogManager.getLogger(ItemContentBitstreamsProcessor.class); public ItemContentBitstreamsProcessor(FrontendUrlService frontendUrlService) { super(frontendUrlService); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemDescribedbyProcessor.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemDescribedbyProcessor.java index 20091e6d0992..7e9569a10445 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemDescribedbyProcessor.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemDescribedbyProcessor.java @@ -10,7 +10,8 @@ import java.util.List; import jakarta.servlet.http.HttpServletRequest; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.signposting.model.LinksetNode; import org.dspace.app.rest.signposting.model.LinksetRelationType; import org.dspace.content.Item; @@ -23,7 +24,7 @@ */ public class ItemDescribedbyProcessor extends ItemSignpostingProcessor { - private static final Logger log = Logger.getLogger(ItemDescribedbyProcessor.class); + private static final Logger log = LogManager.getLogger(ItemDescribedbyProcessor.class); private final ConfigurationService configurationService; @@ -40,8 +41,12 @@ public void addLinkSetNodes(Context context, HttpServletRequest request, String signpostingPath = configurationService.getProperty("signposting.path"); String baseUrl = configurationService.getProperty("dspace.ui.url"); String mimeType = configurationService.getProperty("signposting.describedby.mime-type"); + String profile = configurationService.getProperty("signposting.describedby.profile"); String describedByUrl = baseUrl + "/" + signpostingPath + "/describedby/" + item.getID(); - LinksetNode node = new LinksetNode(describedByUrl, getRelation(), mimeType, buildAnchor(context, item)); + LinksetNode node = profile != null ? + new LinksetNode(describedByUrl, getRelation(), mimeType, profile, buildAnchor(context, item)) : + new LinksetNode(describedByUrl, getRelation(), mimeType, buildAnchor(context, item)); + linksetNodes.add(node); } catch (Exception e) { log.error(e.getMessage(), e); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemLicenseProcessor.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemLicenseProcessor.java index b60ee35d7fe4..6e26d8f1b225 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemLicenseProcessor.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemLicenseProcessor.java @@ -11,7 +11,8 @@ import jakarta.servlet.http.HttpServletRequest; import org.apache.commons.lang3.StringUtils; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.signposting.model.LinksetNode; import org.dspace.app.rest.signposting.model.LinksetRelationType; import org.dspace.content.Item; @@ -25,7 +26,7 @@ */ public class ItemLicenseProcessor extends ItemSignpostingProcessor { - private static final Logger log = Logger.getLogger(ItemLicenseProcessor.class); + private static final Logger log = LogManager.getLogger(ItemLicenseProcessor.class); private final CreativeCommonsService creativeCommonsService = LicenseServiceFactory.getInstance().getCreativeCommonsService(); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemLinksetProcessor.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemLinksetProcessor.java index 2d09e5616171..4e48caf9594f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemLinksetProcessor.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemLinksetProcessor.java @@ -10,7 +10,8 @@ import java.util.List; import jakarta.servlet.http.HttpServletRequest; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.signposting.model.LinksetNode; import org.dspace.app.rest.signposting.model.LinksetRelationType; import org.dspace.content.Item; @@ -23,7 +24,7 @@ */ public class ItemLinksetProcessor extends ItemSignpostingProcessor { - private static final Logger log = Logger.getLogger(ItemLinksetProcessor.class); + private static final Logger log = LogManager.getLogger(ItemLinksetProcessor.class); private final ConfigurationService configurationService; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemTypeProcessor.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemTypeProcessor.java index 49b3612cd92c..f2533a5a9564 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemTypeProcessor.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/processor/item/ItemTypeProcessor.java @@ -11,7 +11,8 @@ import jakarta.servlet.http.HttpServletRequest; import org.apache.commons.lang3.StringUtils; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.signposting.model.LinksetNode; import org.dspace.app.rest.signposting.model.LinksetRelationType; import org.dspace.content.Item; @@ -27,7 +28,7 @@ */ public class ItemTypeProcessor extends ItemSignpostingProcessor { - private static final Logger log = Logger.getLogger(ItemTypeProcessor.class); + private static final Logger log = LogManager.getLogger(ItemTypeProcessor.class); private static final String ABOUT_PAGE_URI = "https://schema.org/AboutPage"; @Autowired diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/service/impl/LinksetServiceImpl.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/service/impl/LinksetServiceImpl.java index 5b28817c9438..42b1c8184957 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/service/impl/LinksetServiceImpl.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/service/impl/LinksetServiceImpl.java @@ -13,7 +13,8 @@ import java.util.List; import jakarta.servlet.http.HttpServletRequest; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.security.BitstreamMetadataReadPermissionEvaluatorPlugin; import org.dspace.app.rest.signposting.model.LinksetNode; import org.dspace.app.rest.signposting.processor.bitstream.BitstreamSignpostingProcessor; @@ -37,7 +38,7 @@ @Service public class LinksetServiceImpl implements LinksetService { - private static final Logger log = Logger.getLogger(LinksetServiceImpl.class); + private static final Logger log = LogManager.getLogger(LinksetServiceImpl.class); @Autowired protected ItemService itemService; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/ItemMetadataValueAddPatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/ItemMetadataValueAddPatchOperation.java index 54dfa6b02c04..8ab9bb451647 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/ItemMetadataValueAddPatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/ItemMetadataValueAddPatchOperation.java @@ -214,7 +214,7 @@ private Integer getRelId(String authority) { private void updateRelationshipPlace(Context context, Item dso, int place, Relationship rs) { try { - if (rs.getLeftItem() == dso) { + if (rs.getLeftItem().equals(dso)) { rs.setLeftPlace(place); } else { rs.setRightPlace(place); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/NotifyServiceAddPatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/NotifyServiceAddPatchOperation.java index ff63fc49fb9c..f2733598983a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/NotifyServiceAddPatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/NotifyServiceAddPatchOperation.java @@ -10,6 +10,7 @@ import java.sql.SQLException; import java.util.LinkedHashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -94,7 +95,14 @@ private NotifyServiceEntity findService(Context context, int serviceId) { private void createNotifyPattern(Context context, Item item, NotifyServiceEntity service, String pattern) { try { - NotifyPatternToTrigger notifyPatternToTrigger = notifyPatternToTriggerService.create(context); + // Check first whether the given pattern, for the current object already exists in the DB + // This accounts for multiple updates via the UI + Optional existingNotifyPatternToTrigger = notifyPatternToTriggerService + .findByItemAndPattern(context, item, pattern).stream() + .filter(obj -> obj.getNotifyService().getID().equals(service.getID())).findFirst(); + + NotifyPatternToTrigger notifyPatternToTrigger = existingNotifyPatternToTrigger.isPresent() + ? existingNotifyPatternToTrigger.get() : notifyPatternToTriggerService.create(context); notifyPatternToTrigger.setItem(item); notifyPatternToTrigger.setNotifyService(service); notifyPatternToTrigger.setPattern(pattern); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/UploadStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/UploadStep.java index e0a9cb17e01c..cc5368add735 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/UploadStep.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/UploadStep.java @@ -11,6 +11,7 @@ import java.io.InputStream; import java.util.List; import java.util.Objects; +import java.util.regex.Pattern; import jakarta.servlet.http.HttpServletRequest; import org.apache.commons.lang3.StringUtils; @@ -48,6 +49,13 @@ public class UploadStep extends AbstractProcessingStep private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(UploadStep.class); + private static final Pattern UPDATE_METADATA_PATTERN = + Pattern.compile("^/sections/[^/]+/files/[^/]+/metadata/[^/]+(/[^/]+)?$"); + private static final Pattern PRIMARY_FLAG_PATTERN = + Pattern.compile("^/sections/[^/]+/primary$"); + private static final Pattern ACCESS_CONDITION_PATTERN = + Pattern.compile("^/sections/[^/]+/files/[^/]+/accessConditions(/[^/]+)?$"); + @Override public DataUpload getData(SubmissionService submissionService, InProgressSubmission obj, SubmissionStepConfig config) throws Exception { @@ -55,13 +63,13 @@ public DataUpload getData(SubmissionService submissionService, InProgressSubmiss DataUpload result = new DataUpload(); List bundles = itemService.getBundles(obj.getItem(), Constants.CONTENT_BUNDLE_NAME); for (Bundle bundle : bundles) { + Bitstream primaryBitstream = bundle.getPrimaryBitstream(); + if (Objects.nonNull(primaryBitstream)) { + result.setPrimary(primaryBitstream.getID()); + } for (Bitstream source : bundle.getBitstreams()) { - Bitstream primaryBitstream = bundle.getPrimaryBitstream(); UploadBitstreamRest b = submissionService.buildUploadBitstream(configurationService, source); result.getFiles().add(b); - if (Objects.nonNull(primaryBitstream)) { - result.setPrimary(primaryBitstream.getID()); - } } } return result; @@ -73,27 +81,27 @@ public void doPatchProcessing(Context context, HttpServletRequest currentRequest String instance = null; if ("remove".equals(op.getOp())) { - if (op.getPath().contains(UPLOAD_STEP_METADATA_PATH)) { + if (UPDATE_METADATA_PATTERN.matcher(op.getPath()).matches()) { instance = UPLOAD_STEP_METADATA_OPERATION_ENTRY; - } else if (op.getPath().contains(UPLOAD_STEP_ACCESSCONDITIONS_OPERATION_ENTRY)) { + } else if (ACCESS_CONDITION_PATTERN.matcher(op.getPath()).matches()) { instance = stepConf.getType() + "." + UPLOAD_STEP_ACCESSCONDITIONS_OPERATION_ENTRY; - } else if (op.getPath().contains(PRIMARY_FLAG_ENTRY)) { + } else if (PRIMARY_FLAG_PATTERN.matcher(op.getPath()).matches()) { instance = PRIMARY_FLAG_ENTRY; } else { instance = UPLOAD_STEP_REMOVE_OPERATION_ENTRY; } } else if ("move".equals(op.getOp())) { - if (op.getPath().contains(UPLOAD_STEP_METADATA_PATH)) { + if (UPDATE_METADATA_PATTERN.matcher(op.getPath()).matches()) { instance = UPLOAD_STEP_METADATA_OPERATION_ENTRY; } else { instance = UPLOAD_STEP_MOVE_OPERATION_ENTRY; } } else { - if (op.getPath().contains(UPLOAD_STEP_ACCESSCONDITIONS_OPERATION_ENTRY)) { + if (ACCESS_CONDITION_PATTERN.matcher(op.getPath()).matches()) { instance = stepConf.getType() + "." + UPLOAD_STEP_ACCESSCONDITIONS_OPERATION_ENTRY; - } else if (op.getPath().contains(UPLOAD_STEP_METADATA_PATH)) { + } else if (UPDATE_METADATA_PATTERN.matcher(op.getPath()).matches()) { instance = UPLOAD_STEP_METADATA_OPERATION_ENTRY; - } else if (op.getPath().contains(PRIMARY_FLAG_ENTRY)) { + } else if (PRIMARY_FLAG_PATTERN.matcher(op.getPath()).matches()) { instance = PRIMARY_FLAG_ENTRY; } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java index 4e5545fabc7f..1ac6a320d9c2 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java @@ -15,7 +15,8 @@ import java.util.UUID; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.tuple.Pair; +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.factory.ContentServiceFactory; @@ -27,6 +28,7 @@ import org.dspace.eperson.service.EPersonService; import org.dspace.utils.DSpace; import org.springframework.core.io.AbstractResource; +import org.springframework.util.DigestUtils; /** * This class acts as a {@link AbstractResource} used by Spring's framework to send the data in a proper and @@ -36,21 +38,24 @@ */ public class BitstreamResource extends AbstractResource { - private String name; - private UUID uuid; - private UUID currentUserUUID; - private boolean shouldGenerateCoverPage; - private byte[] file; - private Set currentSpecialGroups; - - private BitstreamService bitstreamService = ContentServiceFactory.getInstance().getBitstreamService(); - private EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); - private CitationDocumentService citationDocumentService = - new DSpace().getServiceManager() + private static final Logger LOG = LogManager.getLogger(BitstreamResource.class); + + private final String name; + private final UUID uuid; + private final UUID currentUserUUID; + private final boolean shouldGenerateCoverPage; + private final Set currentSpecialGroups; + + private final BitstreamService bitstreamService = ContentServiceFactory.getInstance().getBitstreamService(); + private final EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); + private final CitationDocumentService citationDocumentService = + new DSpace().getServiceManager() .getServicesByType(CitationDocumentService.class).get(0); + private BitstreamDocument document; + public BitstreamResource(String name, UUID uuid, UUID currentUserUUID, Set currentSpecialGroups, - boolean shouldGenerateCoverPage) { + boolean shouldGenerateCoverPage) { this.name = name; this.uuid = uuid; this.currentUserUUID = currentUserUUID; @@ -67,17 +72,15 @@ public BitstreamResource(String name, UUID uuid, UUID currentUserUUID, Set * @return a byte array containing the cover page */ private byte[] getCoverpageByteArray(Context context, Bitstream bitstream) - throws IOException, SQLException, AuthorizeException { - if (file == null) { - try { - Pair citedDocument = citationDocumentService.makeCitedDocument(context, bitstream); - this.file = citedDocument.getLeft(); - } catch (Exception e) { - // Return the original bitstream without the cover page - this.file = IOUtils.toByteArray(bitstreamService.retrieve(context, bitstream)); - } + throws IOException, SQLException, AuthorizeException { + try { + var citedDocument = citationDocumentService.makeCitedDocument(context, bitstream); + return citedDocument.getLeft(); + } catch (Exception e) { + LOG.warn("Could not generate cover page. Will fallback to original document", e); + // Return the original bitstream without the cover page + return IOUtils.toByteArray(bitstreamService.retrieve(context, bitstream)); } - return file; } @Override @@ -87,22 +90,9 @@ public String getDescription() { @Override public InputStream getInputStream() throws IOException { - try (Context context = initializeContext()) { - - Bitstream bitstream = bitstreamService.find(context, uuid); - InputStream out; + fetchDocument(); - if (shouldGenerateCoverPage) { - out = new ByteArrayInputStream(getCoverpageByteArray(context, bitstream)); - } else { - out = bitstreamService.retrieve(context, bitstream); - } - - this.file = null; - return out; - } catch (SQLException | AuthorizeException e) { - throw new IOException(e); - } + return document.inputStream(); } @Override @@ -111,17 +101,60 @@ public String getFilename() { } @Override - public long contentLength() throws IOException { + public long contentLength() { + fetchDocument(); + + return document.length(); + } + + public String getChecksum() { + fetchDocument(); + + return document.etag(); + } + + private void fetchDocument() { + if (document != null) { + return; + } + try (Context context = initializeContext()) { Bitstream bitstream = bitstreamService.find(context, uuid); if (shouldGenerateCoverPage) { - return getCoverpageByteArray(context, bitstream).length; + var coverPage = getCoverpageByteArray(context, bitstream); + + this.document = new BitstreamDocument(etag(bitstream), + coverPage.length, + new ByteArrayInputStream(coverPage)); } else { - return bitstream.getSizeBytes(); + this.document = new BitstreamDocument(bitstream.getChecksum(), + bitstream.getSizeBytes(), + bitstreamService.retrieve(context, bitstream)); } - } catch (SQLException | AuthorizeException e) { - throw new IOException(e); + } catch (SQLException | AuthorizeException | IOException e) { + throw new RuntimeException(e); } + + LOG.debug("fetched document {} {}", shouldGenerateCoverPage, document); + } + + private String etag(Bitstream bitstream) { + + /* Ideally we would calculate the md5 checksum based on the document with coverpage. + However it looks like the coverpage generation is not stable (e.g. if invoked twice it will return + different results). This means we cannot use it for etag calculation/comparison! + + Instead we will create the MD5 based off the original checksum plus fixed prefix. This ensures + that checksums will differ when coverpage is on/off. + However the checksum will _not_ change if the coverpage content changes. + */ + + var content = "coverpage:" + bitstream.getChecksum(); + + StringBuilder builder = new StringBuilder(37); + DigestUtils.appendMd5DigestAsHex(content.getBytes(), builder); + + return builder.toString(); } private Context initializeContext() throws SQLException { @@ -131,4 +164,6 @@ private Context initializeContext() throws SQLException { currentSpecialGroups.forEach(context::setSpecialGroup); return context; } + + private record BitstreamDocument(String etag, long length, InputStream inputStream) {} } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceKernelInitializer.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceKernelInitializer.java index 7dfcd1d76d1d..88a093c0575d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceKernelInitializer.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceKernelInitializer.java @@ -81,6 +81,7 @@ public void initialize(final ConfigurableApplicationContext applicationContext) * Initially look for JNDI Resource called "java:/comp/env/dspace.dir". * If not found, use value provided in "dspace.dir" in Spring Environment */ + @SuppressWarnings("BanJNDI") private String getDSpaceHome(ConfigurableEnvironment environment) { // Load the "dspace.dir" property from Spring Boot's Configuration (application.properties) // This gives us the location of our DSpace configurations, necessary to start the kernel diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/RegexUtils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/RegexUtils.java index b358e785c3b3..df525f679323 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/RegexUtils.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/RegexUtils.java @@ -13,7 +13,7 @@ */ public class RegexUtils { - private RegexUtils(){} + private RegexUtils() {} /** * Regular expression in the request mapping to accept UUID as identifier diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java index f66d794a8fe0..bef6d47dca12 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java @@ -987,21 +987,26 @@ public Object getDSpaceAPIObjectFromRest(Context context, BaseObjectRest restObj */ public BaseObjectRest getBaseObjectRestFromUri(Context context, String uri) throws SQLException { String dspaceUrl = configurationService.getProperty("dspace.server.url"); + String dspaceSSRUrl = configurationService.getProperty("dspace.server.ssr.url", dspaceUrl); // Convert strings to URL objects. // Do this early to check that inputs are well-formed. URL dspaceUrlObject; + URL dspaceUrlSSRObject = null; URL requestUrlObject; try { dspaceUrlObject = new URL(dspaceUrl); requestUrlObject = new URL(uri); + if (StringUtils.isNoneBlank(dspaceSSRUrl)) { + dspaceUrlSSRObject = new URL(dspaceSSRUrl); + } } catch (MalformedURLException ex) { throw new IllegalArgumentException( String.format("Configuration '%s' or request '%s' is malformed", dspaceUrl, uri)); } // Check whether the URI could be valid. - if (!urlIsPrefixOf(dspaceUrl, uri)) { + if (!urlIsPrefixOf(dspaceUrl, uri) && !urlIsPrefixOf(dspaceSSRUrl, uri)) { throw new IllegalArgumentException("the supplied uri is not ours: " + uri); } @@ -1011,10 +1016,15 @@ public BaseObjectRest getBaseObjectRestFromUri(Context context, String uri) thro String[] requestPath = StringUtils.split(requestUrlObject.getPath(), '/'); String[] uriParts = Arrays.copyOfRange(requestPath, dspacePathLength, requestPath.length); + + int dspaceSSRPathLength = StringUtils.split(dspaceUrlSSRObject.getPath(), '/').length; + String[] uriSSRParts = Arrays.copyOfRange(requestPath, dspaceSSRPathLength, + requestPath.length); + if ("api".equalsIgnoreCase(uriParts[0])) { uriParts = Arrays.copyOfRange(uriParts, 1, uriParts.length); } - if (uriParts.length != 3) { + if (uriParts.length != 3 && uriSSRParts.length != 3) { throw new IllegalArgumentException("the supplied uri lacks required path elements: " + uri); } diff --git a/dspace-server-webapp/src/main/resources/application.properties b/dspace-server-webapp/src/main/resources/application.properties index 8233298ef0b0..0d9b9cd836ec 100644 --- a/dspace-server-webapp/src/main/resources/application.properties +++ b/dspace-server-webapp/src/main/resources/application.properties @@ -135,3 +135,15 @@ spring.servlet.multipart.max-file-size = 512MB # Maximum size of a multipart request (i.e. max total size of all files in one request) (default = 10MB) spring.servlet.multipart.max-request-size = 512MB + +################################## +# Spring Boot's HTTP Client configuration (for RestClient and RestTemplate) +# https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.4-Release-Notes#restclient-and-resttemplate +# +# "simple" tells Spring Boot to use JDK's HttpURLConnection (SimpleClientHttpRequestFactory) +# We have to configure this explicitly for DSpace because Spring Boot's autoconfiguration will attempt to +# use Jetty or similar if found on the classpath (and Jetty is on the classpath for Handle Server, etc) +spring.http.client.factory = simple +# "dont_follow" tells Spring Boot not to follow any redirects itself, but instead return the 3xx code to +# the user's browser. +spring.http.client.redirects = dont_follow \ No newline at end of file diff --git a/dspace-server-webapp/src/main/resources/static/index.html b/dspace-server-webapp/src/main/resources/static/index.html index c780286107d8..8262c42c0a5e 100644 --- a/dspace-server-webapp/src/main/resources/static/index.html +++ b/dspace-server-webapp/src/main/resources/static/index.html @@ -321,7 +321,7 @@

Embedded Resources

- +