diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 679543cb01aa..615765967266 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,11 +45,11 @@ jobs: steps: # https://github.com/actions/checkout - name: Checkout codebase - uses: actions/checkout@v4 + uses: actions/checkout@v6 # https://github.com/actions/setup-java - name: Install JDK ${{ matrix.java }} - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: ${{ matrix.java }} distribution: 'temurin' @@ -65,14 +65,14 @@ jobs: # (This artifact is downloadable at the bottom of any job's summary page) - name: Upload Results of ${{ matrix.type }} to Artifact if: ${{ failure() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ matrix.type }} results path: ${{ matrix.resultsdir }} # Upload code coverage report to artifact, so that it can be shared with the 'codecov' job (see below) - name: Upload code coverage report to Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ matrix.type }} coverage report path: 'dspace/target/site/jacoco-aggregate/jacoco.xml' @@ -88,19 +88,19 @@ jobs: # runs-on: ubuntu-latest # steps: # - name: Checkout - # uses: actions/checkout@v4 - + # uses: actions/checkout@v6 + # # # Download artifacts from previous 'tests' job # - name: Download coverage artifacts - # uses: actions/download-artifact@v4 - + # uses: actions/download-artifact@v8 + # # # Now attempt upload to Codecov using its action. # # NOTE: We use a retry action to retry the Codecov upload if it fails the first time. # # # # Retry action: https://github.com/marketplace/actions/retry-action # # Codecov action: https://github.com/codecov/codecov-action # - name: Upload coverage to Codecov.io - # uses: Wandalen/wretry.action@v1.3.0 + # uses: Wandalen/wretry.action@v3.8.0 # with: # action: codecov/codecov-action@v4 # # Ensure codecov-action throws an error when it fails to upload diff --git a/.github/workflows/codescan.yml b/.github/workflows/codescan.yml index 3a563c6fa39c..90d79f9bfd60 100644 --- a/.github/workflows/codescan.yml +++ b/.github/workflows/codescan.yml @@ -35,11 +35,11 @@ jobs: steps: # https://github.com/actions/checkout - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 # https://github.com/actions/setup-java - name: Install JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 17 distribution: 'temurin' @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. # https://github.com/github/codeql-action - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v4 with: # Codescan Javascript as well since a few JS files exist in REST API's interface languages: java, javascript @@ -56,8 +56,8 @@ jobs: # NOTE: Based on testing, this autobuild process works well for DSpace. A custom # DSpace build w/caching (like in build.yml) was about the same speed as autobuild. - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v4 # Perform GitHub Code Scanning. - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 9d32cb119d41..5732a22d8277 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -183,10 +183,10 @@ jobs: steps: # Checkout our codebase (to get access to Docker Compose scripts) - name: Checkout codebase - uses: actions/checkout@v4 + uses: actions/checkout@v6 # Download Docker image artifacts (which were just built by reusable-docker-build.yml) - name: Download Docker image artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: # Download all amd64 Docker images (TAR files) into the /tmp/docker directory pattern: docker-image-*-linux-amd64 @@ -205,8 +205,11 @@ jobs: 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 + # NOTE: Before creating test data, we wait for the backend to become responsive by requesting it every 10 sec. + # Timeout after 5 minutes. This is done to ensure the backend is fully initialized before we create test data. - name: Load test data into Backend run: | + timeout 5m wget --retry-connrefused -t 0 --waitretry=10 http://127.0.0.1:8080/server/api 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. @@ -220,6 +223,19 @@ jobs: result=$(wget -O- -q http://127.0.0.1:8080/server/api/core/collections) echo "$result" echo "$result" | grep -oE "\"Dog in Yard\"," + # Verify basic backend logging is working. + # 1. Access the top communities list. Verify that the "Before request" INFO statement is logged + # 2. Access an invalid endpoint (and ignore 404 response). Verify that a "status:404" WARN statement is logged + - name: Verify backend is logging properly + run: | + wget -O/dev/null -q http://127.0.0.1:8080/server/api/core/communities/search/top + logs=$(docker compose -f docker-compose.yml logs -n 5 dspace) + echo "$logs" + echo "$logs" | grep -o "Before request \[GET /server/api/core/communities/search/top\]" + wget -O/dev/null -q http://127.0.0.1:8080/server/api/does/not/exist || true + logs=$(docker compose -f docker-compose.yml logs -n 5 dspace) + echo "$logs" + echo "$logs" | grep -o "status:404 exception: The repository type does.not was not found" # 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) diff --git a/.github/workflows/issue_opened.yml b/.github/workflows/issue_opened.yml index 0a35a6a95044..c8c421d98f47 100644 --- a/.github/workflows/issue_opened.yml +++ b/.github/workflows/issue_opened.yml @@ -16,7 +16,7 @@ jobs: # Only add to project board if issue is flagged as "needs triage" or has no labels # NOTE: By default we flag new issues as "needs triage" in our issue template if: (contains(github.event.issue.labels.*.name, 'needs triage') || join(github.event.issue.labels.*.name) == '') - uses: actions/add-to-project@v1.0.0 + uses: actions/add-to-project@v1.0.2 # Note, the authentication token below is an ORG level Secret. # It must be created/recreated manually via a personal access token with admin:org, project, public_repo permissions # See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token diff --git a/.github/workflows/port_merged_pull_request.yml b/.github/workflows/port_merged_pull_request.yml index 857f22755e49..676ad45ba263 100644 --- a/.github/workflows/port_merged_pull_request.yml +++ b/.github/workflows/port_merged_pull_request.yml @@ -23,11 +23,11 @@ jobs: if: github.event.pull_request.merged steps: # Checkout code - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # Port PR to other branch (ONLY if labeled with "port to") # See https://github.com/korthout/backport-action - name: Create backport pull requests - uses: korthout/backport-action@v2 + uses: korthout/backport-action@v4 with: # Trigger based on a "port to [branch]" label on PR # (This label must specify the branch name to port to) diff --git a/.github/workflows/pull_request_opened.yml b/.github/workflows/pull_request_opened.yml index bbac52af2438..e2b6e8ba9c2c 100644 --- a/.github/workflows/pull_request_opened.yml +++ b/.github/workflows/pull_request_opened.yml @@ -21,4 +21,4 @@ jobs: # Assign the PR to whomever created it. This is useful for visualizing assignments on project boards # See https://github.com/toshimaru/auto-author-assign - name: Assign PR to creator - uses: toshimaru/auto-author-assign@v2.1.0 + uses: toshimaru/auto-author-assign@v3.0.1 diff --git a/.github/workflows/reusable-docker-build.yml b/.github/workflows/reusable-docker-build.yml index 0c3261da95da..768adb8f1602 100644 --- a/.github/workflows/reusable-docker-build.yml +++ b/.github/workflows/reusable-docker-build.yml @@ -109,13 +109,13 @@ jobs: # https://github.com/actions/checkout - name: Checkout codebase - uses: actions/checkout@v4 + uses: actions/checkout@v6 # https://github.com/docker/login-action # NOTE: This login occurs for BOTH non-PRs or PRs. PRs *must* also login to access private images from GHCR # during the build process - name: Login to ${{ env.DOCKER_BUILD_REGISTRY }} - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ${{ env.DOCKER_BUILD_REGISTRY }} username: ${{ github.repository_owner }} @@ -123,13 +123,13 @@ jobs: # https://github.com/docker/setup-buildx-action - name: Setup Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 # https://github.com/docker/metadata-action # 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 + uses: docker/metadata-action@v6 with: images: ${{ env.DOCKER_BUILD_REGISTRY }}/${{ env.IMAGE_NAME }} tags: ${{ env.IMAGE_TAGS }} @@ -147,7 +147,7 @@ jobs: - name: Build and push image to ${{ env.DOCKER_BUILD_REGISTRY }} if: ${{ ! matrix.isPr }} id: docker_build - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: build-contexts: | ${{ inputs.dockerfile_additional_contexts }} @@ -164,7 +164,7 @@ jobs: # 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 + cache-to: type=gha,scope=${{ inputs.build_id }},mode=min # Export the digest of Docker build locally - name: Export Docker build digest @@ -178,7 +178,7 @@ jobs: # (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 + uses: actions/upload-artifact@v7 with: name: digests-${{ inputs.build_id }}-${{ env.ARCH_NAME }} path: /tmp/digests/* @@ -201,7 +201,7 @@ jobs: # 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 + uses: docker/build-push-action@v7 with: build-contexts: | ${{ inputs.dockerfile_additional_contexts }} @@ -216,7 +216,7 @@ jobs: # 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 + cache-to: type=gha,scope=${{ inputs.build_id }},mode=min # Export image to a local TAR file outputs: type=docker,dest=/tmp/${{ inputs.build_id }}.tar @@ -224,7 +224,7 @@ jobs: # 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 + uses: actions/upload-artifact@v7 with: name: docker-image-${{ inputs.build_id }}-${{ env.ARCH_NAME }} path: /tmp/${{ inputs.build_id }}.tar @@ -245,7 +245,7 @@ jobs: - docker-build steps: - name: Download Docker build digests - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: /tmp/digests # Download digests for both AMD64 and ARM64 into same directory @@ -253,18 +253,18 @@ jobs: merge-multiple: true - name: Login to ${{ env.DOCKER_BUILD_REGISTRY }} - uses: docker/login-action@v3 + uses: docker/login-action@v4 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 + uses: docker/setup-buildx-action@v4 - name: Add Docker metadata for image id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ env.DOCKER_BUILD_REGISTRY }}/${{ env.IMAGE_NAME }} tags: ${{ env.IMAGE_TAGS }} @@ -298,14 +298,17 @@ jobs: # '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' + run: | + export REGCTL_VERSION=v0.9.2 + mkdir -p bin + curl -sSLo bin/regctl https://github.com/regclient/regclient/releases/download/${REGCTL_VERSION}/regctl-linux-amd64 + chmod a+x bin/regctl + echo "$(pwd)/bin" >> $GITHUB_PATH # This recreates Docker tags for DockerHub - name: Add Docker metadata for image id: meta_dockerhub - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ env.IMAGE_NAME }} tags: ${{ env.IMAGE_TAGS }} @@ -313,7 +316,7 @@ jobs: # 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 + uses: docker/login-action@v4 with: registry: ${{ env.DOCKER_BUILD_REGISTRY }} username: ${{ github.repository_owner }} @@ -321,7 +324,7 @@ jobs: # Login to DockerHub, since this is where we are copying *to* - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_ACCESS_TOKEN }} diff --git a/Dockerfile b/Dockerfile index 382db1ed3784..bf0a938c5192 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,26 +46,23 @@ 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 -# Download and install 'ant' -RUN mkdir $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 +# Install Apache Ant +RUN apt-get update \ + && apt-get install -y --no-install-recommends ant \ + && apt-get purge -y --auto-remove \ + && rm -rf /var/lib/apt/lists/* # Run necessary 'ant' deploy scripts RUN ant init_installation update_configs update_code update_webapps # Step 3 - Start up DSpace via Runnable JAR FROM docker.io/eclipse-temurin:${JDK_VERSION} -# NOTE: DSPACE_INSTALL must align with the "dspace.dir" default configuration. -ENV DSPACE_INSTALL=/dspace +# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables. +# See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml +# "dspace__P__dir" is setting the value of the "dspace.dir" configuration. This is our installation directory. +ENV dspace__P__dir=/dspace # Copy the /dspace directory from 'ant_build' container to /dspace in this container -COPY --from=ant_build /dspace $DSPACE_INSTALL -WORKDIR $DSPACE_INSTALL +COPY --from=ant_build /dspace $dspace__P__dir +WORKDIR $dspace__P__dir # Need host command for "[dspace]/bin/make-handle-config" RUN apt-get update \ && apt-get install -y --no-install-recommends host \ @@ -96,5 +93,5 @@ RUN apt-get update && \ COPY dspace/src/main/docker/cron/postfix.sh /usr/local/bin/postfix.sh # End UMD Customization -# On startup, run DSpace Runnable JAR -ENTRYPOINT ["java", "-jar", "webapps/server-boot.jar", "--dspace.dir=$DSPACE_INSTALL"] +# On startup, run DSpace Runnable JAR (uses the "dspace.dir" setting defined in "dspace__P__dir" env variable) +ENTRYPOINT ["java", "-jar", "webapps/server-boot.jar"] diff --git a/Dockerfile.ant b/Dockerfile.ant index 4bcec223f186..8d5759dc6d8e 100644 --- a/Dockerfile.ant +++ b/Dockerfile.ant @@ -4,13 +4,8 @@ ARG JDK_VERSION=17 FROM docker.io/eclipse-temurin:${JDK_VERSION} AS ant_build -# Create the initial install deployment using ANT -ENV ANT_VERSION=1.10.13 -ENV ANT_HOME=/tmp/ant-$ANT_VERSION -ENV PATH=$ANT_HOME/bin:$PATH -# Download and install 'ant' -RUN mkdir $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 +# Install Apache Ant +RUN apt-get update \ + && apt-get install -y --no-install-recommends ant \ + && apt-get purge -y --auto-remove \ + && rm -rf /var/lib/apt/lists/* diff --git a/Dockerfile.cli b/Dockerfile.cli index 72913d5e5e9d..82ae1a64dfa2 100644 --- a/Dockerfile.cli +++ b/Dockerfile.cli @@ -44,25 +44,23 @@ 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 -# Download and install 'ant' -RUN mkdir $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 +# Install Apache Ant +RUN apt-get update \ + && apt-get install -y --no-install-recommends ant \ + && apt-get purge -y --auto-remove \ + && rm -rf /var/lib/apt/lists/* # Run necessary 'ant' deploy scripts RUN ant init_installation update_configs update_code # Step 3 - Run jdk FROM docker.io/eclipse-temurin:${JDK_VERSION} -# NOTE: DSPACE_INSTALL must align with the "dspace.dir" default configuration. -ENV DSPACE_INSTALL=/dspace +# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables. +# See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml +# "dspace__P__dir" is setting the value of the "dspace.dir" configuration. This is our installation directory. +ENV dspace__P__dir=/dspace # Copy the /dspace directory from 'ant_build' container to /dspace in this container -COPY --from=ant_build /dspace $DSPACE_INSTALL +COPY --from=ant_build /dspace $dspace__P__dir +WORKDIR $dspace__P__dir # Give java extra memory (1GB) ENV JAVA_OPTS=-Xmx1000m # Install unzip for AIPs @@ -70,3 +68,8 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends unzip \ && apt-get purge -y --auto-remove \ && rm -rf /var/lib/apt/lists/* + +# On startup, run DSpace commandline script +ENTRYPOINT ["./bin/dspace"] +# By default just pass 'help' command to ./bin/dspace +CMD ["help"] diff --git a/Dockerfile.dev b/Dockerfile.dev index 90e1e579a18c..6e5e72e9c4f9 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -56,11 +56,13 @@ RUN ant init_installation update_configs update_code update_webapps # Step 3 - Start up DSpace via Runnable JAR FROM docker.io/eclipse-temurin:${JDK_VERSION} -# NOTE: DSPACE_INSTALL must align with the "dspace.dir" default configuration. -ENV DSPACE_INSTALL=/dspace +# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables. +# See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml +# "dspace__P__dir" is setting the value of the "dspace.dir" configuration. This is our installation directory. +ENV dspace__P__dir=/dspace # Copy the /dspace directory from 'ant_build' container to /dspace in this container -COPY --from=ant_build /dspace $DSPACE_INSTALL -WORKDIR $DSPACE_INSTALL +COPY --from=ant_build /dspace $dspace__P__dir +WORKDIR $dspace__P__dir # Need host command for "[dspace]/bin/make-handle-config" RUN apt-get update \ && apt-get install -y --no-install-recommends host \ @@ -87,8 +89,9 @@ RUN apt-get update && \ vim \ python3-lxml \ jq && \ - mkfifo /var/spool/postfix/public/pickup && \ ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +COPY dspace/src/main/docker/cron/postfix.sh /usr/local/bin/postfix.sh # End UMD Customization -# On startup, run DSpace Runnable JAR -ENTRYPOINT ["java", "-jar", "webapps/server-boot.jar", "--dspace.dir=$DSPACE_INSTALL"] +# On startup, run DSpace Runnable JAR (uses the "dspace.dir" setting defined in "dspace__P__dir" env variable) +ENTRYPOINT ["java", "-jar", "webapps/server-boot.jar"] diff --git a/Dockerfile.dev-additions b/Dockerfile.dev-additions index 64bcd62b688c..9d365e3d21e2 100644 --- a/Dockerfile.dev-additions +++ b/Dockerfile.dev-additions @@ -58,11 +58,13 @@ RUN ant init_installation update_configs update_code update_webapps # Step 3 - Start up DSpace via Runnable JAR FROM docker.io/eclipse-temurin:${JDK_VERSION} -# NOTE: DSPACE_INSTALL must align with the "dspace.dir" default configuration. -ENV DSPACE_INSTALL=/dspace +# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables. +# See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml +# "dspace__P__dir" is setting the value of the "dspace.dir" configuration. This is our installation directory. +ENV dspace__P__dir=/dspace # Copy the /dspace directory from 'ant_build' container to /dspace in this container -COPY --from=ant_build /dspace $DSPACE_INSTALL -WORKDIR $DSPACE_INSTALL +COPY --from=ant_build /dspace $dspace__P__dir +WORKDIR $dspace__P__dir # Need host command for "[dspace]/bin/make-handle-config" # UMD Customization # Commenting out, because no need for "make-handle-config" in dev environment @@ -86,9 +88,8 @@ RUN apt-get update && \ jq # Create the directories needed for Proquest ETD loading -RUN mkdir -p $DSPACE_INSTALL/proquest/incoming $DSPACE_INSTALL/proquest/processed \ - $DSPACE_INSTALL/proquest/csv $DSPACE_INSTALL/proquest/marc +RUN mkdir -p $dspace__P__dir/proquest/incoming $dspace__P__dir/proquest/processed \ + $dspace__P__dir/proquest/csv $dspace__P__dir/proquest/marc # End UMD Customization - -# On startup, run DSpace Runnable JAR -ENTRYPOINT ["java", "-jar", "webapps/server-boot.jar", "--dspace.dir=$DSPACE_INSTALL"] +# On startup, run DSpace Runnable JAR (uses the "dspace.dir" setting defined in "dspace__P__dir" env variable) +ENTRYPOINT ["java", "-jar", "webapps/server-boot.jar"] diff --git a/Dockerfile.test b/Dockerfile.test index c9627e439fd7..79e2e5d9eec1 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -39,26 +39,23 @@ 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 -# Download and install 'ant' -RUN mkdir $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 +# Install Apache Ant +RUN apt-get update \ + && apt-get install -y --no-install-recommends ant \ + && apt-get purge -y --auto-remove \ + && rm -rf /var/lib/apt/lists/* # Run necessary 'ant' deploy scripts RUN ant init_installation update_configs update_code update_webapps # Step 3 - Start up DSpace via Runnable JAR FROM docker.io/eclipse-temurin:${JDK_VERSION} -# NOTE: DSPACE_INSTALL must align with the "dspace.dir" default configuration. -ENV DSPACE_INSTALL=/dspace +# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables. +# See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml +# "dspace__P__dir" is setting the value of the "dspace.dir" configuration. This is our installation directory. +ENV dspace__P__dir=/dspace # Copy the /dspace directory from 'ant_build' container to /dspace in this container -COPY --from=ant_build /dspace $DSPACE_INSTALL -WORKDIR $DSPACE_INSTALL +COPY --from=ant_build /dspace $dspace__P__dir +WORKDIR $dspace__P__dir # Need host command for "[dspace]/bin/make-handle-config" RUN apt-get update \ && apt-get install -y --no-install-recommends host \ @@ -70,5 +67,5 @@ EXPOSE 8080 8000 ENV JAVA_OPTS=-Xmx2000m # 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"] +# On startup, run DSpace Runnable JAR (uses the "dspace.dir" setting defined in "dspace__P__dir" env variable) +ENTRYPOINT ["java", "-jar", "webapps/server-boot.jar"] diff --git a/LICENSES_THIRD_PARTY b/LICENSES_THIRD_PARTY index 5d99bd7e426c..c65827035560 100644 --- a/LICENSES_THIRD_PARTY +++ b/LICENSES_THIRD_PARTY @@ -21,35 +21,34 @@ 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.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) + * S3Mock - Testsupport - Testcontainers (com.adobe.testing:s3mock-testcontainers:4.12.4 - https://www.github.com/adobe/S3Mock/s3mock-testsupport-reactor/s3mock-testcontainers) * 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) * 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) + * ClassMate (com.fasterxml:classmate:1.7.3 - https://github.com/FasterXML/java-classmate) + * Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.21 - https://github.com/FasterXML/jackson) + * Jackson-core (com.fasterxml.jackson.core:jackson-core:2.21.3 - https://github.com/FasterXML/jackson-core) + * jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.21.3 - https://github.com/FasterXML/jackson) * 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.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 datatype: jdk8 (com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.21.2 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8) + * Jackson datatype: JSR310 (com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.21.3 - 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.19.1 - https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names) + * Jackson-module-parameter-names (com.fasterxml.jackson.module:jackson-module-parameter-names:2.21.2 - 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.8 - https://github.com/ben-manes/caffeine) * JSON.simple (com.github.cliftonlabs:json-simple:3.0.2 - https://cliftonlabs.github.io/json-simple/) + * docker-java-api (com.github.docker-java:docker-java-api:3.7.1 - https://github.com/docker-java/docker-java) + * docker-java-transport (com.github.docker-java:docker-java-transport:3.7.1 - https://github.com/docker-java/docker-java) + * docker-java-transport-zerodep (com.github.docker-java:docker-java-transport-zerodep:3.7.1 - https://github.com/docker-java/docker-java) * 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) @@ -60,25 +59,26 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * uri-template (com.github.java-json-tools:uri-template:0.10 - https://github.com/java-json-tools/uri-template) * JCIP Annotations under Apache License (com.github.stephenc.jcip:jcip-annotations:1.0-1 - http://stephenc.github.com/jcip-annotations) * FindBugs-jsr305 (com.google.code.findbugs:jsr305:3.0.2 - http://findbugs.sourceforge.net/) - * 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) + * Gson (com.google.code.gson:gson:2.14.0 - https://github.com/google/gson) + * error-prone annotations (com.google.errorprone:error_prone_annotations:2.42.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.1.3-jre - https://github.com/google/guava) + * Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.3 - https://github.com/google/guava/failureaccess) + * Guava: Google Core Libraries for Java (com.google.guava:guava:33.6.0-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) * 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/) + * J2ObjC Annotations (com.google.j2objc:j2objc-annotations:3.1 - https://github.com/google/j2objc/) * libphonenumber (com.googlecode.libphonenumber:libphonenumber:8.11.1 - https://github.com/google/libphonenumber/) - * Jackcess (com.healthmarketscience.jackcess:jackcess:4.0.8 - https://jackcess.sourceforge.io) + * Jackcess (com.healthmarketscience.jackcess:jackcess:4.0.10 - 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) + * json-path (com.jayway.jsonpath:json-path:2.10.0 - https://github.com/jayway/JsonPath) + * json-path-assert (com.jayway.jsonpath:json-path-assert:2.10.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) * 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) + * opencsv (com.opencsv:opencsv:5.12.0 - 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) @@ -88,26 +88,17 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * 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) - * akka-actor (com.typesafe.akka:akka-actor_2.13:2.5.31 - https://akka.io/) - * akka-http-core (com.typesafe.akka:akka-http-core_2.13:10.1.12 - https://akka.io) - * akka-http (com.typesafe.akka:akka-http_2.13:10.1.12 - https://akka.io) - * akka-parsing (com.typesafe.akka:akka-parsing_2.13:10.1.12 - https://akka.io) - * akka-protobuf (com.typesafe.akka:akka-protobuf_2.13:2.5.31 - https://akka.io/) - * akka-stream (com.typesafe.akka:akka-stream_2.13:2.5.31 - https://akka.io/) - * 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.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 CLI (commons-cli:commons-cli:1.11.0 - https://commons.apache.org/proper/commons-cli/) + * Apache Commons Codec (commons-codec:commons-codec:1.22.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.19.0 - https://commons.apache.org/proper/commons-io/) + * Apache Commons IO (commons-io:commons-io:2.22.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.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/) + * Apache Commons Logging (commons-logging:commons-logging:1.3.6 - https://commons.apache.org/proper/commons-logging/) + * Apache Commons Validator (commons-validator:commons-validator:1.10.1 - https://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) @@ -117,10 +108,10 @@ 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.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) + * micrometer-commons (io.micrometer:micrometer-commons:1.15.11 - https://github.com/micrometer-metrics/micrometer) + * micrometer-core (io.micrometer:micrometer-core:1.15.11 - https://github.com/micrometer-metrics/micrometer) + * micrometer-jakarta9 (io.micrometer:micrometer-jakarta9:1.15.11 - https://github.com/micrometer-metrics/micrometer) + * micrometer-observation (io.micrometer:micrometer-observation:1.15.11 - 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.99.Final - https://netty.io/netty-codec/) * Netty/Codec/HTTP (io.netty:netty-codec-http:4.1.86.Final - https://netty.io/netty-codec-http/) @@ -168,39 +159,38 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * 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) * jdbm (jdbm:jdbm:1.0 - no url defined) - * Joda-Time (joda-time:joda-time:2.12.7 - https://www.joda.org/joda-time/) + * Joda-Time (joda-time:joda-time:2.10.5 - 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) + * Java Native Access (net.java.dev.jna:jna:5.18.1 - https://github.com/java-native-access/jna) * 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/) + * ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.6.0 - https://urielch.github.io/) + * JSON Small and Fast Parser (net.minidev:json-smart:2.6.0 - https://urielch.github.io/) * Abdera Core (org.apache.abdera:abdera-core:1.1.3 - http://abdera.apache.org/abdera-core) * I18N Libraries (org.apache.abdera:abdera-i18n:1.1.3 - http://abdera.apache.org) * Abdera Parser (org.apache.abdera:abdera-parser:1.1.3 - http://abdera.apache.org/abdera-parser) - * Apache Ant Core (org.apache.ant:ant:1.10.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) + * Apache Ant Core (org.apache.ant:ant:1.10.17 - https://ant.apache.org/) + * Apache Ant Launcher (org.apache.ant:ant-launcher:1.10.17 - https://ant.apache.org/) + * Apache Commons BCEL (org.apache.bcel:bcel:6.12.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.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 Compress (org.apache.commons:commons-compress:1.28.0 - https://commons.apache.org/proper/commons-compress/) + * Apache Commons Configuration (org.apache.commons:commons-configuration2:2.15.0 - https://commons.apache.org/proper/commons-configuration/) + * Apache Commons CSV (org.apache.commons:commons-csv:1.14.1 - https://commons.apache.org/proper/commons-csv/) + * Apache Commons DBCP (org.apache.commons:commons-dbcp2:2.14.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.17.0 - https://commons.apache.org/proper/commons-lang/) + * Apache Commons Exec (org.apache.commons:commons-exec:1.6.0 - https://commons.apache.org/proper/commons-exec/) + * Apache Commons Lang (org.apache.commons:commons-lang3:3.20.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.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) + * Apache Commons Pool (org.apache.commons:commons-pool2:2.13.1 - https://commons.apache.org/proper/commons-pool/) + * Apache Commons Text (org.apache.commons:commons-text:1.15.0 - 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) @@ -214,13 +204,13 @@ 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.5 - https://hc.apache.org/httpcomponents-client-5.5.x/5.5/httpclient5/) + * Apache HttpClient (org.apache.httpcomponents.client5:httpclient5:5.6.1 - https://hc.apache.org/httpcomponents-client-5.5.x/5.6.1/httpclient5/) * Apache HttpComponents Core HTTP/1.1 (org.apache.httpcomponents.core5:httpcore5:5.1.3 - https://hc.apache.org/httpcomponents-core-5.1.x/5.1.3/httpcore5/) - * Apache HttpComponents Core HTTP/1.1 (org.apache.httpcomponents.core5:httpcore5:5.3.4 - https://hc.apache.org/httpcomponents-core-5.3.x/5.3.4/httpcore5/) + * Apache HttpComponents Core HTTP/1.1 (org.apache.httpcomponents.core5:httpcore5:5.4 - https://hc.apache.org/httpcomponents-core-5.4.x/5.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.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 HttpComponents Core HTTP/2 (org.apache.httpcomponents.core5:httpcore5-h2:5.4 - https://hc.apache.org/httpcomponents-core-5.4.x/5.4/httpcore5-h2/) + * Apache James :: Mime4j :: Core (org.apache.james:apache-mime4j-core:0.8.14 - http://james.apache.org/mime4j/apache-mime4j-core) + * Apache James :: Mime4j :: DOM (org.apache.james:apache-mime4j-dom:0.8.13 - 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/) @@ -242,12 +232,12 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * 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.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 API (org.apache.logging.log4j:log4j-api:2.25.4 - https://logging.apache.org/log4j/2.x/) + * Apache Log4j Core (org.apache.logging.log4j:log4j-core:2.25.4 - https://logging.apache.org/log4j/2.x/) * 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.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/) + * SLF4J 2 Provider for Log4j API (org.apache.logging.log4j:log4j-slf4j2-impl:2.25.4 - https://logging.apache.org/log4j/2.x/) * 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) @@ -272,48 +262,49 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * 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/) + * Apache FontBox (org.apache.pdfbox:fontbox:3.0.7 - 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.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 PDFBox (org.apache.pdfbox:pdfbox:3.0.7 - https://www.apache.org/pdfbox-parent/pdfbox/) + * Apache PDFBox io (org.apache.pdfbox:pdfbox-io:3.0.7 - https://www.apache.org/pdfbox-parent/pdfbox-io/) + * Apache PDFBox tools (org.apache.pdfbox:pdfbox-tools:3.0.7 - https://www.apache.org/pdfbox-parent/pdfbox-tools/) + * Apache XmpBox (org.apache.pdfbox:xmpbox:3.0.7 - https://www.apache.org/pdfbox-parent/xmpbox/) + * Apache POI - Common (org.apache.poi:poi:5.5.1 - https://poi.apache.org/) + * Apache POI - API based on OPC and OOXML schemas (org.apache.poi:poi-ooxml:5.5.1 - https://poi.apache.org/) + * Apache POI - OOXML schemas (full) (org.apache.poi:poi-ooxml-full:5.5.1 - https://poi.apache.org/) + * Apache POI (org.apache.poi:poi-scratchpad:5.5.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.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 Tika core (org.apache.tika:tika-core:3.3.0 - https://tika.apache.org/) + * Apache Tika Apple parser module (org.apache.tika:tika-parser-apple-module:3.3.0 - https://tika.apache.org/tika-parser-apple-module/) + * Apache Tika audiovideo parser module (org.apache.tika:tika-parser-audiovideo-module:3.3.0 - https://tika.apache.org/tika-parser-audiovideo-module/) + * Apache Tika cad parser module (org.apache.tika:tika-parser-cad-module:3.3.0 - https://tika.apache.org/tika-parser-cad-module/) + * Apache Tika code parser module (org.apache.tika:tika-parser-code-module:3.3.0 - https://tika.apache.org/tika-parser-code-module/) + * Apache Tika crypto parser module (org.apache.tika:tika-parser-crypto-module:3.3.0 - https://tika.apache.org/tika-parser-crypto-module/) + * Apache Tika digest commons (org.apache.tika:tika-parser-digest-commons:3.3.0 - https://tika.apache.org/tika-parser-digest-commons/) + * Apache Tika font parser module (org.apache.tika:tika-parser-font-module:3.3.0 - https://tika.apache.org/tika-parser-font-module/) + * Apache Tika html parser module (org.apache.tika:tika-parser-html-module:3.3.0 - https://tika.apache.org/tika-parser-html-module/) + * Apache Tika image parser module (org.apache.tika:tika-parser-image-module:3.3.0 - https://tika.apache.org/tika-parser-image-module/) + * Apache Tika mail commons (org.apache.tika:tika-parser-mail-commons:3.3.0 - https://tika.apache.org/tika-parser-mail-commons/) + * Apache Tika mail parser module (org.apache.tika:tika-parser-mail-module:3.3.0 - https://tika.apache.org/tika-parser-mail-module/) + * Apache Tika Microsoft parser module (org.apache.tika:tika-parser-microsoft-module:3.3.0 - https://tika.apache.org/tika-parser-microsoft-module/) + * Apache Tika miscellaneous office format parser module (org.apache.tika:tika-parser-miscoffice-module:3.3.0 - https://tika.apache.org/tika-parser-miscoffice-module/) + * Apache Tika news parser module (org.apache.tika:tika-parser-news-module:3.3.0 - https://tika.apache.org/tika-parser-news-module/) + * Apache Tika OCR parser module (org.apache.tika:tika-parser-ocr-module:3.3.0 - https://tika.apache.org/tika-parser-ocr-module/) + * Apache Tika PDF parser module (org.apache.tika:tika-parser-pdf-module:3.3.0 - https://tika.apache.org/tika-parser-pdf-module/) + * Apache Tika package parser module (org.apache.tika:tika-parser-pkg-module:3.3.0 - https://tika.apache.org/tika-parser-pkg-module/) + * Apache Tika text parser module (org.apache.tika:tika-parser-text-module:3.3.0 - https://tika.apache.org/tika-parser-text-module/) + * Apache Tika WARC parser module (org.apache.tika:tika-parser-webarchive-module:3.3.0 - https://tika.apache.org/tika-parser-webarchive-module/) + * Apache Tika XML parser module (org.apache.tika:tika-parser-xml-module:3.3.0 - https://tika.apache.org/tika-parser-xml-module/) + * Apache Tika XMP commons (org.apache.tika:tika-parser-xmp-commons:3.3.0 - https://tika.apache.org/tika-parser-xmp-commons/) + * Apache Tika ZIP commons (org.apache.tika:tika-parser-zip-commons:3.3.0 - https://tika.apache.org/tika-parser-zip-commons/) + * Apache Tika standard parser package (org.apache.tika:tika-parsers-standard-package:3.3.0 - 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.54 - https://tomcat.apache.org/) + * tomcat-embed-el (org.apache.tomcat.embed:tomcat-embed-el:10.1.54 - https://tomcat.apache.org/) + * tomcat-embed-websocket (org.apache.tomcat.embed:tomcat-embed-websocket:10.1.54 - 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/) @@ -323,12 +314,11 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * 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.27.3 - https://assertj.github.io/doc/#assertj-core) + * AssertJ Core (org.assertj:assertj-core:3.27.7 - https://assertj.github.io/doc/#assertj-core) * Evo Inflector (org.atteo:evo-inflector:1.3 - http://atteo.org/static/evo-inflector) * 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) * jems (org.dmfs:jems:1.18 - https://github.com/dmfs/jems) * rfc3986-uri (org.dmfs:rfc3986-uri:0.8.1 - https://github.com/dmfs/uri-toolkit) @@ -344,124 +334,151 @@ 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.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 :: Deployers (org.eclipse.jetty:jetty-deploy:9.4.58.v20250814 - https://jetty.org/jetty-deploy/) + * Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.58.v20250814 - https://jetty.org/jetty-http/) + * Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.58.v20250814 - 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.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 :: Security (org.eclipse.jetty:jetty-security:9.4.58.v20250814 - https://jetty.org/jetty-security/) + * Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.58.v20250814 - https://jetty.org/jetty-server/) + * Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.58.v20250814 - 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.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 :: Utilities (org.eclipse.jetty:jetty-util:9.4.58.v20250814 - https://jetty.org/jetty-util/) + * Jetty :: Utilities :: Ajax(JSON) (org.eclipse.jetty:jetty-util-ajax:9.4.58.v20250814 - https://jetty.org/jetty-util-ajax/) + * Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.58.v20250814 - 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.57.v20241219 - https://jetty.org/jetty-xml/) + * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.58.v20250814 - 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.57.v20241219 - https://jetty.org/http2-parent/http2-common/) + * Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:9.4.58.v20250814 - 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) + * Ehcache (org.ehcache:ehcache:3.12.0 - http://ehcache.org) * 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-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-core-client (org.glassfish.jersey.core:jersey-client:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.11 - 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) + * Hibernate Validator Engine (org.hibernate.validator:hibernate-validator:8.0.3.Final - https://hibernate.org/validator) + * Hibernate Validator Portable Extension (org.hibernate.validator:hibernate-validator-cdi:8.0.3.Final - https://hibernate.org/validator) * 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.30.2-GA - https://www.javassist.org/) - * JBoss Logging 3 (org.jboss.logging:jboss-logging:3.6.1.Final - http://www.jboss.org) + * JBoss Logging 3 (org.jboss.logging:jboss-logging:3.5.0.Final - http://www.jboss.org) * JDOM (org.jdom:jdom2:2.0.6.1 - http://www.jdom.org) - * IntelliJ IDEA Annotations (org.jetbrains:annotations:13.0 - http://www.jetbrains.org) + * JetBrains Java Annotations (org.jetbrains:annotations:17.0.0 - https://github.com/JetBrains/java-annotations) * 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/) + * JSpecify annotations (org.jspecify:jspecify:1.0.0 - http://jspecify.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.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) + * jwarc (org.netpreserve:jwarc:0.35.0 - https://github.com/iipc/jwarc) * Objenesis (org.objenesis:objenesis:3.2 - http://objenesis.org/objenesis) - * 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.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.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 AOP (org.springframework:spring-aop:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring Beans (org.springframework:spring-beans:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring Context (org.springframework:spring-context:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring Context Support (org.springframework:spring-context-support:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring Core (org.springframework:spring-core:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring Expression Language (SpEL) (org.springframework:spring-expression:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring JDBC (org.springframework:spring-jdbc:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring Object/Relational Mapping (org.springframework:spring-orm:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring TestContext Framework (org.springframework:spring-test:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring Transaction (org.springframework:spring-tx:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring Web (org.springframework:spring-web:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring Web MVC (org.springframework:spring-webmvc:6.2.18 - https://github.com/spring-projects/spring-framework) + * spring-boot (org.springframework.boot:spring-boot:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-actuator (org.springframework.boot:spring-boot-actuator:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-actuator-autoconfigure (org.springframework.boot:spring-boot-actuator-autoconfigure:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-autoconfigure (org.springframework.boot:spring-boot-autoconfigure:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter (org.springframework.boot:spring-boot-starter:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-actuator (org.springframework.boot:spring-boot-starter-actuator:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-aop (org.springframework.boot:spring-boot-starter-aop:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-cache (org.springframework.boot:spring-boot-starter-cache:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-data-rest (org.springframework.boot:spring-boot-starter-data-rest:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-json (org.springframework.boot:spring-boot-starter-json:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-log4j2 (org.springframework.boot:spring-boot-starter-log4j2:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-security (org.springframework.boot:spring-boot-starter-security:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-test (org.springframework.boot:spring-boot-starter-test:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-thymeleaf (org.springframework.boot:spring-boot-starter-thymeleaf:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-tomcat (org.springframework.boot:spring-boot-starter-tomcat:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-web (org.springframework.boot:spring-boot-starter-web:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-test (org.springframework.boot:spring-boot-test:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-test-autoconfigure (org.springframework.boot:spring-boot-test-autoconfigure:3.5.14 - https://spring.io/projects/spring-boot) + * Spring Data Core (org.springframework.data:spring-data-commons:3.5.11 - https://spring.io/projects/spring-data) + * Spring Data REST - Core (org.springframework.data:spring-data-rest-core:4.5.11 - 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.11 - https://www.spring.io/spring-data/spring-data-rest-parent/spring-data-rest-webmvc) + * Spring HATEOAS (org.springframework.hateoas:spring-hateoas:2.5.2 - https://github.com/spring-projects/spring-hateoas) + * spring-ldap-core (org.springframework.ldap:spring-ldap-core:3.3.7 - https://spring.io/projects/spring-ldap) * 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.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) + * spring-security-config (org.springframework.security:spring-security-config:6.5.10 - https://spring.io/projects/spring-security) + * spring-security-core (org.springframework.security:spring-security-core:6.5.10 - https://spring.io/projects/spring-security) + * spring-security-crypto (org.springframework.security:spring-security-crypto:6.5.10 - https://spring.io/projects/spring-security) + * spring-security-test (org.springframework.security:spring-security-test:6.5.10 - https://spring.io/projects/spring-security) + * spring-security-web (org.springframework.security:spring-security-web:6.5.10 - https://spring.io/projects/spring-security) + * thymeleaf (org.thymeleaf:thymeleaf:3.1.5.RELEASE - http://www.thymeleaf.org/thymeleaf-lib/thymeleaf) + * thymeleaf-spring6 (org.thymeleaf:thymeleaf-spring6:3.1.5.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.2 - https://www.xmlunit.org/) + * org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.10.4 - https://www.xmlunit.org/) + * org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.11.0 - 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) + * AWS Java SDK :: Annotations (software.amazon.awssdk:annotations:2.43.2 - https://aws.amazon.com/sdkforjava/core/annotations) + * AWS Java SDK :: Arns (software.amazon.awssdk:arns:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Auth (software.amazon.awssdk:auth:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: AWS Core (software.amazon.awssdk:aws-core:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Core :: Protocols :: AWS Query Protocol (software.amazon.awssdk:aws-query-protocol:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Core :: Protocols :: AWS Xml Protocol (software.amazon.awssdk:aws-xml-protocol:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Checksums (software.amazon.awssdk:checksums:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Checksums SPI (software.amazon.awssdk:checksums-spi:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: AWS CRT Core (software.amazon.awssdk:crt-core:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Endpoints SPI (software.amazon.awssdk:endpoints-spi:2.43.2 - https://aws.amazon.com/sdkforjava/core/endpoints-spi) + * AWS Java SDK :: HTTP Auth (software.amazon.awssdk:http-auth:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: HTTP Auth AWS (software.amazon.awssdk:http-auth-aws:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: HTTP Auth Event Stream (software.amazon.awssdk:http-auth-aws-eventstream:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: HTTP Auth SPI (software.amazon.awssdk:http-auth-spi:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: HTTP Client Interface (software.amazon.awssdk:http-client-spi:2.43.2 - https://aws.amazon.com/sdkforjava/http-client-spi) + * AWS Java SDK :: Identity SPI (software.amazon.awssdk:identity-spi:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Core :: Protocols :: Json Utils (software.amazon.awssdk:json-utils:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Metrics SPI (software.amazon.awssdk:metrics-spi:2.43.2 - https://aws.amazon.com/sdkforjava/core/metrics-spi) + * AWS Java SDK :: Profiles (software.amazon.awssdk:profiles:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Core :: Protocols :: Protocol Core (software.amazon.awssdk:protocol-core:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Regions (software.amazon.awssdk:regions:2.43.2 - https://aws.amazon.com/sdkforjava/core/regions) + * AWS Java SDK :: Retries (software.amazon.awssdk:retries:2.43.2 - https://aws.amazon.com/sdkforjava/core/retries) + * AWS Java SDK :: Retries API (software.amazon.awssdk:retries-spi:2.43.2 - https://aws.amazon.com/sdkforjava/core/retries-spi) + * AWS Java SDK :: Services :: Amazon S3 (software.amazon.awssdk:s3:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: SDK Core (software.amazon.awssdk:sdk-core:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Third Party :: Jackson-core (software.amazon.awssdk:third-party-jackson-core:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Utilities (software.amazon.awssdk:utils:2.43.2 - https://aws.amazon.com/sdkforjava/utils) + * AWS Java SDK :: Utils Lite (software.amazon.awssdk:utils-lite:2.43.2 - https://aws.amazon.com/sdkforjava) + * software.amazon.awssdk.crt:aws-crt (software.amazon.awssdk.crt:aws-crt:0.45.2 - https://github.com/awslabs/aws-crt-java) + * AWS Event Stream (software.amazon.eventstream:eventstream:1.0.1 - https://github.com/awslabs/aws-eventstream-java) * Xerces2-j (xerces:xercesImpl:2.12.2 - https://xerces.apache.org/xerces2-j/) + BSD 2-Clause License: + + * zstd-jni (com.github.luben:zstd-jni:1.5.7-4 - https://github.com/luben/zstd-jni) + BSD License: * Adobe XMPCore (com.adobe.xmp:xmpcore:6.1.11 - https://www.adobe.com/devnet/xmp/library/eula-xmp-library-java.html) @@ -473,15 +490,15 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * 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/) * 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) + * dnsjava (dnsjava:dnsjava:3.6.4 - https://github.com/dnsjava/dnsjava) + * jaxen (jaxen:jaxen:2.0.1 - https://jaxen-xpath.github.io/jaxen/jaxen/) * 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-core-client (org.glassfish.jersey.core:jersey-client:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.11 - 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/) @@ -491,39 +508,34 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * 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/) - * PostgreSQL JDBC Driver (org.postgresql:postgresql:42.7.7 - https://jdbc.postgresql.org) + * PostgreSQL JDBC Driver (org.postgresql:postgresql:42.7.11 - 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) + * XZ for Java (org.tukaani:xz:1.12 - https://tukaani.org/xz/java.html) * XMLUnit for Java (xmlunit:xmlunit:1.3 - http://xmlunit.sourceforge.net/) - CC0: - - * reactive-streams (org.reactivestreams:reactive-streams:1.0.2 - http://www.reactive-streams.org/) - Common Development and Distribution License (CDDL): * JavaMail API (no providers) (com.sun.mail:mailapi:1.6.2 - http://javaee.github.io/javamail/mailapi) * Old JAXB Core (com.sun.xml.bind:jaxb-core:2.3.0.1 - http://jaxb.java.net/jaxb-bundles/jaxb-core) * Old JAXB Runtime (com.sun.xml.bind:jaxb-impl:2.3.1 - http://jaxb.java.net/jaxb-bundles/jaxb-impl) * Jakarta Annotations API (jakarta.annotation:jakarta.annotation-api: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 Mail API (jakarta.mail:jakarta.mail-api:2.1.5 - https://projects.eclipse.org/projects/ee4j/jakarta.mail-api) * 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) * Java Servlet API (javax.servlet:javax.servlet-api:3.1.0 - http://servlet-spec.java.net) * javax.transaction API (javax.transaction:javax.transaction-api:1.3 - http://jta-spec.java.net) * jaxb-api (javax.xml.bind:jaxb-api:2.3.1 - https://github.com/javaee/jaxb-spec/jaxb-api) - * JHighlight (org.codelibs:jhighlight:1.1.0 - https://github.com/codelibs/jhighlight) - * Angus Mail default provider (org.eclipse.angus:jakarta.mail:2.0.3 - http://eclipse-ee4j.github.io/angus-mail/jakarta.mail) + * JHighlight (org.codelibs:jhighlight:1.1.1 - https://github.com/codelibs/jhighlight) + * Angus Mail default provider (org.eclipse.angus:jakarta.mail:2.0.5 - http://eclipse-ee4j.github.io/angus-mail/jakarta.mail) * 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.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-core-client (org.glassfish.jersey.core:jersey-client:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.11 - 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: @@ -538,17 +550,17 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines Eclipse Distribution License, Version 1.0: * istack common utility code runtime (com.sun.istack:istack-commons-runtime:4.1.2 - https://projects.eclipse.org/projects/ee4j/istack-commons/istack-commons-runtime) - * Jakarta Activation API (jakarta.activation:jakarta.activation-api:2.1.3 - https://github.com/jakartaee/jaf-api) - * Jakarta Mail API (jakarta.mail:jakarta.mail-api:2.1.3 - https://projects.eclipse.org/projects/ee4j/jakarta.mail-api) + * Jakarta Activation API (jakarta.activation:jakarta.activation-api:2.1.4 - https://github.com/jakartaee/jaf-api) + * Jakarta Mail API (jakarta.mail:jakarta.mail-api:2.1.5 - https://projects.eclipse.org/projects/ee4j/jakarta.mail-api) * Jakarta Persistence API (jakarta.persistence:jakarta.persistence-api:3.1.0 - https://github.com/eclipse-ee4j/jpa-api) - * Jakarta XML Binding API (jakarta.xml.bind:jakarta.xml.bind-api:4.0.2 - https://github.com/jakartaee/jaxb-api/jakarta.xml.bind-api) - * Angus Activation Registries (org.eclipse.angus:angus-activation:2.0.2 - https://github.com/eclipse-ee4j/angus-activation/angus-activation) - * Angus Mail default provider (org.eclipse.angus:jakarta.mail:2.0.3 - http://eclipse-ee4j.github.io/angus-mail/jakarta.mail) - * JAXB Core (org.glassfish.jaxb:jaxb-core:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) - * JAXB Runtime (org.glassfish.jaxb:jaxb-runtime:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) - * TXW2 Runtime (org.glassfish.jaxb:txw2:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) - * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.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) + * Jakarta XML Binding API (jakarta.xml.bind:jakarta.xml.bind-api:4.0.5 - https://github.com/jakartaee/jaxb-api/jakarta.xml.bind-api) + * Angus Activation Registries (org.eclipse.angus:angus-activation:2.0.3 - https://github.com/eclipse-ee4j/angus-activation/angus-activation) + * Angus Mail default provider (org.eclipse.angus:jakarta.mail:2.0.5 - http://eclipse-ee4j.github.io/angus-mail/jakarta.mail) + * JAXB Core (org.glassfish.jaxb:jaxb-core:4.0.8 - https://eclipse-ee4j.github.io/jaxb-ri/) + * JAXB Runtime (org.glassfish.jaxb:jaxb-runtime:4.0.8 - https://eclipse-ee4j.github.io/jaxb-ri/) + * TXW2 Runtime (org.glassfish.jaxb:txw2:4.0.8 - https://eclipse-ee4j.github.io/jaxb-ri/) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.11 - 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) @@ -557,16 +569,16 @@ 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.3.232 - https://h2database.com) + * H2 Database Engine (com.h2database:h2:2.4.240 - https://h2database.com) * 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 Mail API (jakarta.mail:jakarta.mail-api:2.1.5 - 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.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.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) + * AspectJ Weaver (org.aspectj:aspectjweaver:1.9.25.1 - https://www.eclipse.org/aspectj/) + * Angus Mail default provider (org.eclipse.angus:jakarta.mail:2.0.5 - http://eclipse-ee4j.github.io/angus-mail/jakarta.mail) * Jetty :: Apache JSP Implementation (org.eclipse.jetty:apache-jsp:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Apache :: JSTL module (org.eclipse.jetty:apache-jstl:9.4.15.v20190215 - http://tomcat.apache.org/taglibs/standard/) * Jetty :: ALPN :: Client (org.eclipse.jetty:jetty-alpn-client:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-client) @@ -579,27 +591,27 @@ 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.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 :: Deployers (org.eclipse.jetty:jetty-deploy:9.4.58.v20250814 - https://jetty.org/jetty-deploy/) + * Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.58.v20250814 - https://jetty.org/jetty-http/) + * Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.58.v20250814 - 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.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 :: Security (org.eclipse.jetty:jetty-security:9.4.58.v20250814 - https://jetty.org/jetty-security/) + * Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.58.v20250814 - https://jetty.org/jetty-server/) + * Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.58.v20250814 - 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.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 :: Utilities (org.eclipse.jetty:jetty-util:9.4.58.v20250814 - https://jetty.org/jetty-util/) + * Jetty :: Utilities :: Ajax(JSON) (org.eclipse.jetty:jetty-util-ajax:9.4.58.v20250814 - https://jetty.org/jetty-util-ajax/) + * Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.58.v20250814 - 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.57.v20241219 - https://jetty.org/jetty-xml/) + * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.58.v20250814 - 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.57.v20241219 - https://jetty.org/http2-parent/http2-common/) + * Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:9.4.58.v20250814 - 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) @@ -611,13 +623,10 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * 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.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-core-client (org.glassfish.jersey.core:jersey-client:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.11 - 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) @@ -640,14 +649,14 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * 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) * FindBugs-Annotations (com.google.code.findbugs:annotations:3.0.1u2 - http://findbugs.sourceforge.net/) - * JHighlight (org.codelibs:jhighlight:1.1.0 - https://github.com/codelibs/jhighlight) + * JHighlight (org.codelibs:jhighlight:1.1.1 - https://github.com/codelibs/jhighlight) * Hibernate Commons Annotations (org.hibernate.common:hibernate-commons-annotations:6.0.6.Final - http://hibernate.org) - * Hibernate ORM - hibernate-core (org.hibernate.orm:hibernate-core:6.4.8.Final - https://hibernate.org/orm) - * Hibernate ORM - hibernate-jcache (org.hibernate.orm:hibernate-jcache:6.4.8.Final - https://hibernate.org/orm) - * Hibernate ORM - hibernate-jpamodelgen (org.hibernate.orm:hibernate-jpamodelgen:6.4.8.Final - https://hibernate.org/orm) + * Hibernate ORM - hibernate-core (org.hibernate.orm:hibernate-core:6.4.10.Final - https://hibernate.org/orm) + * Hibernate ORM - hibernate-jcache (org.hibernate.orm:hibernate-jcache:6.4.10.Final - https://hibernate.org/orm) + * Hibernate ORM - hibernate-jpamodelgen (org.hibernate.orm:hibernate-jpamodelgen:6.4.10.Final - https://hibernate.org/orm) * im4java (org.im4java:im4java:1.4.0 - http://sourceforge.net/projects/im4java/) * Javassist (org.javassist:javassist:3.30.2-GA - https://www.javassist.org/) - * XOM (xom:xom:1.3.9 - https://xom.nu) + * XOM (xom:xom:1.4.1 - https://xom.nu) Go License: @@ -661,28 +670,34 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Simple Magic (com.j256.simplemagic:simplemagic:1.17 - https://256stuff.com/sources/simplemagic/) + LGPL-2.1-or-later: + + * Java Native Access (net.java.dev.jna:jna:5.18.1 - https://github.com/java-native-access/jna) + MIT License: * dexx (com.github.andrewoma.dexx:collection:0.7 - https://github.com/andrewoma/dexx) - * better-files (com.github.pathikrit:better-files_2.13:3.9.1 - https://github.com/pathikrit/better-files) * Java SemVer (com.github.zafarkhaja:java-semver:0.9.0 - https://github.com/zafarkhaja/jsemver) - * dd-plist (com.googlecode.plist:dd-plist:1.28 - http://www.github.com/3breadt/dd-plist) + * dd-plist (com.googlecode.plist:dd-plist:1.29 - http://www.github.com/3breadt/dd-plist) * 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 JavaMail S/MIME APIs (org.bouncycastle:bcmail-jdk18on:1.80 - https://www.bouncycastle.org/download/bouncy-castle-java/) + * Bouncy Castle JavaMail Jakarta S/MIME APIs (org.bouncycastle:bcjmail-jdk18on:1.83 - 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.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) + * Checker Qual (org.checkerframework:checker-qual:3.23.0 - https://checkerframework.org) + * Checker Qual (org.checkerframework:checker-qual:3.37.0 - https://checkerframework.org/) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.11 - 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) + * jsoup Java HTML Parser (org.jsoup:jsoup:1.22.1 - https://jsoup.org/) * 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) + * Duct Tape (org.rnorth.duct-tape:duct-tape:1.0.8 - https://github.com/rnorth/duct-tape) * SLF4J API Module (org.slf4j:slf4j-api:2.0.17 - http://www.slf4j.org) + * Testcontainers Core (org.testcontainers:testcontainers:2.0.5 - https://java.testcontainers.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) @@ -690,31 +705,39 @@ 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.42.0 - https://www.webjars.org) + * core-js (org.webjars.npm:core-js:3.49.0 - https://www.webjars.org) * @json-editor/json-editor (org.webjars.npm:json-editor__json-editor:2.15.2 - https://www.webjars.org) + MIT-0: + + * reactive-streams (org.reactivestreams:reactive-streams:1.0.4 - http://www.reactive-streams.org/) + Mozilla Public License: * 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) + * H2 Database Engine (com.h2database:h2:2.4.240 - 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.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-core-client (org.glassfish.jersey.core:jersey-client:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.11 - 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.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) + The Apache Software License, version 2.0: + + * picocli (info.picocli:picocli:4.7.7 - https://picocli.info) + UnRar License: - * Java Unrar (com.github.junrar:junrar:7.5.5 - https://github.com/junrar/junrar) + * Java Unrar (com.github.junrar:junrar:7.5.8 - https://github.com/junrar/junrar) Unicode/ICU License: @@ -722,12 +745,12 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines W3C license: - * 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-core-client (org.glassfish.jersey.core:jersey-client:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.11 - 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.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-core-client (org.glassfish.jersey.core:jersey-client:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.11 - 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 1d93abe49948..53e7e8cc3461 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ Additional support options are at https://wiki.lyrasis.org/display/DSPACE/Suppor 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 -[Registered Service Providers](http://www.dspace.org/service-providers). +[Registered Service Providers](https://dspace.org/registered-service-providers/). ## Issue Tracker diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml index 46bd9ca80d62..963165f75cef 100644 --- a/checkstyle-suppressions.xml +++ b/checkstyle-suppressions.xml @@ -8,4 +8,5 @@ on JMockIt Expectations blocks and similar. See https://github.com/checkstyle/checkstyle/issues/3739 --> + diff --git a/docker-compose-cli.yml b/docker-compose-cli.yml index 942354f4d5c8..565d9b6bde91 100644 --- a/docker-compose-cli.yml +++ b/docker-compose-cli.yml @@ -18,24 +18,18 @@ services: # See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml # __P__ => "." (e.g. dspace__P__dir => dspace.dir) # __D__ => "-" (e.g. google__D__metadata => google-metadata) - # dspace.dir: Must match with Dockerfile's DSPACE_INSTALL directory. - dspace__P__dir: /dspace # db.url: Ensure we are using the 'dspacedb' image for our database # UMD Customization - db__P__url: 'jdbc:postgresql://dspacedb:5432/drum' + db__P__url: {db__P__url:-jdbc:postgresql://dspacedb:5432/drum} # End UMD Customization # solr.server: Ensure we are using the 'dspacesolr' image for Solr - solr__P__server: http://dspacesolr:8983/solr + solr__P__server: ${solr__P__server:-http://dspacesolr:8983/solr} volumes: # Keep DSpace assetstore directory between reboots - assetstore:/dspace/assetstore # Mount local [src]/dspace/config/ to container. This syncs your local configs with container # NOTE: Environment variables specified above will OVERRIDE any configs in local.cfg or dspace.cfg - ./dspace/config:/dspace/config - entrypoint: /dspace/bin/dspace - command: help - tty: true - stdin_open: true volumes: assetstore: diff --git a/docker-compose.yml b/docker-compose.yml index 36ff1cf6f63b..5f0c0fd6d263 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,8 @@ networks: # Define a custom subnet for our DSpace network, so that we can easily trust requests from host to container. # If you customize this value, be sure to customize the 'proxies.trusted.ipranges' env variable below. - subnet: 172.23.0.0/16 + # Explicitly set external=false because this script creates the network. + external: false services: # UMD Customization # Nginx server configuration for supporting HTTPS connections from the @@ -29,26 +31,31 @@ services: # See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml # __P__ => "." (e.g. dspace__P__dir => dspace.dir) # __D__ => "-" (e.g. google__D__metadata => google-metadata) - # dspace.dir: Must match with Dockerfile's DSPACE_INSTALL directory. - dspace__P__dir: /dspace - # Uncomment to set a non-default value for dspace.server.url or dspace.ui.url + # Uncomment to set a non-default value for dspace.dir, dspace.server.url or dspace.ui.url + # dspace__P__dir: /dspace # UMD Customization dspace__P__server__P__url: https://api.drum-local.lib.umd.edu/server dspace__P__ui__P__url: https://drum-local.lib.umd.edu:4000 # End UMD Customization - dspace__P__name: 'DSpace Started with Docker Compose' + # Set SSR URL to the Docker container name so that UI can contact container directly in Production mode. + # (This is necessary for docker-compose-angular.yml as it uses production mode by default) + dspace__P__server__P__ssr__P__url: ${dspace__P__server__P__ssr__P__url:-http://dspace:8080/server} + dspace__P__name: ${dspace__P__name:-DSpace Started with Docker Compose} + # UMD Customization + # Customization for Matomo - remove when updating to DSpace 9 or later + # matomo.tracker.url: Ensure we are using the 'matomo' image for Matomo + matomo__P__tracker__P__url: ${matomo__P__tracker__P__url:-http://matomo} + # End UMD Customization # db.url: Ensure we are using the 'dspacedb' image for our database # UMD Customization - db__P__url: 'jdbc:postgresql://dspacedb:5432/drum' + db__P__url: ${db__P__url:-jdbc:postgresql://dspacedb:5432/drum} # End UMD Customization # solr.server: Ensure we are using the 'dspacesolr' image for Solr - solr__P__server: http://dspacesolr:8983/solr - # matomo.tracker.url: Ensure we are using the 'matomo' image for Matomo - matomo__P__tracker__P__url: http://matomo + solr__P__server: ${solr__P__server:-http://dspacesolr:8983/solr} # proxies.trusted.ipranges: This setting is required for a REST API running in Docker to trust requests # 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 + proxies__P__trusted__P__ipranges: ${proxies__P__trusted__P__ipranges:-172.23.0} + LOGGING_CONFIG: ${LOGGING_CONFIG:-/dspace/config/log4j2-container.xml} # UMD Customization JPDA_OPTS: "-agentlib:jdwp=transport=dt_socket,address=0.0.0.0:8000,server=y,suspend=n" # End UMD Customization @@ -69,8 +76,6 @@ services: target: 8080 - published: 8000 target: 8000 - stdin_open: true - tty: true volumes: # Keep DSpace assetstore directory between reboots - assetstore:/dspace/assetstore @@ -88,7 +93,7 @@ services: while (! /dev/null 2>&1; do sleep 1; done; /dspace/bin/dspace database migrate # UMD Customization - java $${JPDA_OPTS} -jar /dspace/webapps/server-boot.jar --dspace.dir=/dspace + java $${JPDA_OPTS} -jar /dspace/webapps/server-boot.jar # End UMD Customization # DSpace PostgreSQL database container dspacedb: @@ -112,8 +117,6 @@ services: ports: - published: 5432 target: 5432 - stdin_open: true - tty: true volumes: # Keep Postgres data directory between reboots - pgdata:/pgdata @@ -136,8 +139,6 @@ services: ports: - published: 8983 target: 8983 - stdin_open: true - tty: true working_dir: /var/solr/data volumes: # Keep Solr data directory between reboots diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 3803df9ec322..2a17be8981c4 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -12,7 +12,7 @@ org.dspace dspace-parent - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT .. @@ -99,24 +99,10 @@ - - org.codehaus.mojo - build-helper-maven-plugin - 3.6.1 - - - validate - - maven-version - - - - - org.codehaus.mojo buildnumber-maven-plugin - 3.2.1 + 3.3.0 UNKNOWN_REVISION @@ -400,16 +386,13 @@ ${hibernate-validator.version} + + org.springframework.ldap + spring-ldap-core + org.springframework spring-orm - - - - org.springframework - spring-jcl - - @@ -438,6 +421,11 @@ org.bouncycastle bcprov-jdk15on + + + javax.annotation + javax.annotation-api + @@ -651,7 +639,7 @@ dnsjava dnsjava - 3.6.3 + 3.6.4 @@ -660,6 +648,7 @@ 1.1.1 + com.google.guava guava @@ -736,9 +725,25 @@ - com.amazonaws - aws-java-sdk-s3 - 1.12.785 + software.amazon.awssdk + s3 + 2.43.2 + + + software.amazon.awssdk + netty-nio-client + + + software.amazon.awssdk + apache-client + + + + + + software.amazon.awssdk.crt + aws-crt + 0.45.2 com.opencsv opencsv - 5.11.1 + 5.12.0 @@ -791,14 +797,14 @@ org.xmlunit xmlunit-core - 2.10.2 + 2.11.0 test org.apache.bcel bcel - 6.10.0 + 6.12.0 test @@ -851,21 +857,26 @@ - io.findify - s3mock_2.13 - 0.2.6 + com.adobe.testing + s3mock-testcontainers + 4.12.4 test - com.amazonawsl - aws-java-sdk-s3 - - - com.amazonaws - aws-java-sdk-s3 + org.testcontainers + testcontainers + + + org.testcontainers + testcontainers + 2.0.5 + test + com.squareup.okhttp3 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 30f68efaf3cb..59e75059c94f 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 @@ -18,6 +18,7 @@ import java.sql.SQLException; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.time.ZoneOffset; import java.util.Arrays; import java.util.Date; import java.util.Iterator; @@ -154,7 +155,7 @@ public void internalRun() throws Exception { } ObjectMapper mapper = new ObjectMapper(); - mapper.setTimeZone(TimeZone.getTimeZone("UTC")); + mapper.setTimeZone(TimeZone.getTimeZone(ZoneOffset.UTC)); BulkAccessControlInput accessControl; context = new Context(Context.Mode.BATCH_EDIT); setEPerson(context); 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 3533a2397b3d..89caa6c15286 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 @@ -25,6 +25,7 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.dspace.app.util.MetadataExposureServiceImpl; import org.dspace.authority.AuthorityValue; import org.dspace.authority.factory.AuthorityServiceFactory; import org.dspace.authority.service.AuthorityValueService; @@ -321,20 +322,7 @@ protected void init() { // Set the metadata fields to ignore ignore = new HashMap<>(); - // Specify default values - String[] defaultValues = - new String[] { - "dc.date.accessioned", "dc.date.available", "dc.date.updated", "dc.description.provenance" - }; - String[] toIgnoreArray = - DSpaceServicesFactory.getInstance() - .getConfigurationService() - .getArrayProperty("bulkedit.ignore-on-export", defaultValues); - for (String toIgnoreString : toIgnoreArray) { - if (!"".equals(toIgnoreString.trim())) { - ignore.put(toIgnoreString.trim(), toIgnoreString.trim()); - } - } + getConfiguredIgnoreFields(); } /** @@ -352,6 +340,40 @@ public boolean hasActions() { return false; } + /** + * Sets the ignored fields with 'bulkedit.ignore-on-export' + * + * Also adds 'metadata.hide.*' fields to ignored if 'bulkedit.ignore-on-export.include-metadata-hide' is true + */ + private void getConfiguredIgnoreFields() { + // Specify default values + String[] defaultValues = + new String[] { + "dc.date.accessioned", "dc.date.available", "dc.date.updated", "dc.description.provenance" + }; + String[] toIgnoreArray = + DSpaceServicesFactory.getInstance() + .getConfigurationService() + .getArrayProperty("bulkedit.ignore-on-export", defaultValues); + + boolean ignoreHiddenMetadata = DSpaceServicesFactory.getInstance().getConfigurationService() + .getBooleanProperty("bulkedit.ignore-on-export.include-metadata-hide", true); + if (ignoreHiddenMetadata) { + List hiddenMetadata = DSpaceServicesFactory.getInstance().getConfigurationService() + .getPropertyKeys(MetadataExposureServiceImpl.CONFIG_PREFIX); + for (String hiddenMetadataKey : hiddenMetadata) { + String key = hiddenMetadataKey.split(MetadataExposureServiceImpl.CONFIG_PREFIX)[1]; + ignore.put(key.trim(), key.trim()); + } + } + + for (String toIgnoreString : toIgnoreArray) { + if (!"".equals(toIgnoreString.trim())) { + ignore.put(toIgnoreString.trim(), toIgnoreString.trim()); + } + } + } + /** * Set the value separator for multiple values stored in one csv value. * 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 e4bbe335d63e..689df4701a96 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 @@ -14,6 +14,8 @@ import java.util.List; import java.util.UUID; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.DefaultParser.Builder; import org.apache.commons.cli.ParseException; import org.dspace.content.Item; import org.dspace.content.MetadataDSpaceCsvExportServiceImpl; @@ -167,4 +169,14 @@ public IndexableObject resolveScope(Context context, String id) throws SQLExcept } return scopeObj; } + + @Override + protected StepResult parse(String[] args) throws ParseException { + commandLine = new DefaultParser().parse(getScriptConfiguration().getOptions(), args); + Builder builder = new DefaultParser().builder(); + builder.setStripLeadingAndTrailingQuotes(false); + commandLine = builder.build().parse(getScriptConfiguration().getOptions(), args); + setup(); + return StepResult.Continue; + } } diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java index e8cf42b47c1b..37a19c6435d2 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java @@ -358,6 +358,16 @@ public List runImport(Context c, boolean change, // Process each change rowCount = 1; + + int maxItems = configurationService.getIntProperty("bulkedit.import.max.items", 1000); + int numItems = toImport.size(); + if (numItems > maxItems && maxItems > 0) { + throw new MetadataImportException( + "Import contains " + numItems + " items, which exceeds the configured " + + "maximum of " + maxItems + ". You can change this limit by setting " + + "'bulkedit.import.max.items' in your local configuration."); + } + for (DSpaceCSVLine line : toImport) { // Resolve target references to other items populateRefAndRowMap(line, line.getID()); @@ -494,7 +504,7 @@ public List runImport(Context c, boolean change, // Check it has an owning collection List collections = line.get("collection"); - if (collections == null) { + if (collections == null || collections.isEmpty()) { throw new MetadataImportException( "New items must have a 'collection' assigned in the form of a handle"); } diff --git a/dspace-api/src/main/java/org/dspace/app/checker/ChecksumChecker.java b/dspace-api/src/main/java/org/dspace/app/checker/ChecksumChecker.java index ec024c345263..160d23e32204 100644 --- a/dspace-api/src/main/java/org/dspace/app/checker/ChecksumChecker.java +++ b/dspace-api/src/main/java/org/dspace/app/checker/ChecksumChecker.java @@ -98,7 +98,7 @@ public static void main(String[] args) throws SQLException { options.addOption("h", "help", false, "Help"); options.addOption("d", "duration", true, "Checking duration"); options.addOption("c", "count", true, "Check count"); - options.addOption("a", "handle", true, "Specify a handle to check"); + options.addOption("i", "handle", true, "Specify a handle to check"); options.addOption("v", "verbose", false, "Report all processing"); Option option; @@ -106,7 +106,7 @@ public static void main(String[] args) throws SQLException { option = Option.builder("b") .longOpt("bitstream-ids") .hasArgs() - .desc("Space separated list of bitstream ids") + .desc("Space separated list of bitstream UUIDs") .build(); options.addOption(option); @@ -132,6 +132,17 @@ public static void main(String[] args) throws SQLException { try { context = new Context(); + int mutuallyExclusiveOpts = 0; + for (char c : new char[]{'l', 'L', 'd', 'b', 'i','c'}) { + if (line.hasOption(c)) { + mutuallyExclusiveOpts++; + } + } + if (mutuallyExclusiveOpts > 1) { + System.err.println("Please use only one option of -l, -L, -d, -b, -i, or -c"); + LOG.error("Please use only one option of -l, -L, -d, -b, -i, or -c"); + System.exit(1); + } // Prune stage if (line.hasOption('p')) { @@ -169,13 +180,13 @@ public static void main(String[] args) throws SQLException { bitstreams.add(bitstreamService.find(context, UUID.fromString(ids[i]))); } catch (NumberFormatException nfe) { System.err.println("The following argument: " + ids[i] - + " is not an integer"); + + " is not an UUID"); System.exit(0); } } dispatcher = new IteratorDispatcher(bitstreams.iterator()); - } else if (line.hasOption('a')) { - dispatcher = new HandleDispatcher(context, line.getOptionValue('a')); + } else if (line.hasOption('i')) { + dispatcher = new HandleDispatcher(context, line.getOptionValue('i')); } else if (line.hasOption('d')) { // run checker process for specified duration try { @@ -185,6 +196,8 @@ public static void main(String[] args) throws SQLException { + Utils.parseDuration(line .getOptionValue('d')))); } catch (Exception e) { + System.err.println("Couldn't parse " + line.getOptionValue('d') + + " as a duration"); LOG.fatal("Couldn't parse " + line.getOptionValue('d') + " as a duration: ", e); System.exit(0); @@ -228,18 +241,24 @@ public static void main(String[] args) throws SQLException { private static void printHelp(Options options) { HelpFormatter myhelp = new HelpFormatter(); - myhelp.printHelp("Checksum Checker\n", options); - System.out.println("\nSpecify a duration for checker process, using s(seconds)," - + "m(minutes), or h(hours): ChecksumChecker -d 30s" - + " OR ChecksumChecker -d 30m" - + " OR ChecksumChecker -d 2h"); - System.out.println("\nSpecify bitstream IDs: ChecksumChecker -b 13 15 17 20"); - System.out.println("\nLoop once through all bitstreams: " - + "ChecksumChecker -l"); - System.out.println("\nLoop continuously through all bitstreams: ChecksumChecker -L"); - System.out.println("\nCheck a defined number of bitstreams: ChecksumChecker -c 10"); - System.out.println("\nReport all processing (verbose)(default reports only errors): ChecksumChecker -v"); - System.out.println("\nDefault (no arguments) is equivalent to '-c 1'"); + myhelp.printHelp("checker\n", options); + System.out.println("\nChecksum Checker usage examples:"); + System.out.println("\nThe following options are mutually exclusive:"); + System.out.println(" - Specify a duration for checker process, using s(seconds)," + + "m(minutes), or h(hours): checker -d 30s" + + " OR checker -d 30m" + + " OR checker -d 2h"); + System.out.println(" - Specify bitstream UUIDs: checker -b 550e8400-e29b-41d4-a716-446655440000" + + " f3f2e850-b5d4-11ef-ac7e-96584d5248b2"); + System.out.println(" - Specify handle: checker -i 12345/100"); + System.out.println(" - Loop once through all bitstreams: " + + "checker -l"); + System.out.println(" - Loop continuously through all bitstreams: checker -L"); + System.out.println(" - Check a defined number of bitstreams: checker -c 10"); + System.out.println("\nThe following options can be used in combination with others above:"); + System.out.println(" - Report all processing to checker.log (by default logs only errors): checker -v"); + System.out.println(" - Prune old results from the database: checker -p"); + System.out.println("\nDefault (no arguments) is equivalent to 'checker -c 1'\n"); System.exit(0); } diff --git a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java index 9eaabc20e862..d50b44fd8d4c 100644 --- a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java @@ -352,7 +352,7 @@ protected void writeHandle(Context c, Item i, File destDir) /** * Create the 'collections' file. List handles of all Collections which - * contain this Item. The "owning" Collection is listed first. + * contain this Item. The "owning" Collection is listed first. * * @param item list collections holding this Item. * @param destDir write the file here. @@ -363,12 +363,14 @@ protected void writeCollections(Item item, File destDir) File outFile = new File(destDir, "collections"); if (outFile.createNewFile()) { try (PrintWriter out = new PrintWriter(new FileWriter(outFile))) { - String ownerHandle = item.getOwningCollection().getHandle(); - out.println(ownerHandle); + Collection owningCollection = item.getOwningCollection(); + // The owning collection is null for workspace and workflow items + if (owningCollection != null) { + out.println(owningCollection.getHandle()); + } for (Collection collection : item.getCollections()) { - String collectionHandle = collection.getHandle(); - if (!collectionHandle.equals(ownerHandle)) { - out.println(collectionHandle); + if (!collection.equals(owningCollection)) { + out.println(collection.getHandle()); } } } diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImport.java b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImport.java index b32de11f7a7f..33487bc8e35a 100644 --- a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImport.java +++ b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImport.java @@ -22,6 +22,7 @@ import org.apache.commons.cli.ParseException; import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.tika.Tika; import org.dspace.app.itemimport.factory.ItemImportServiceFactory; @@ -333,33 +334,38 @@ protected void process(Context context, ItemImportService itemImportService, protected void readZip(Context context, ItemImportService itemImportService) throws Exception { Optional optionalFileStream = Optional.empty(); Optional validationFileStream = Optional.empty(); - if (!remoteUrl) { - // manage zip via upload - optionalFileStream = handler.getFileStream(context, zipfilename); - validationFileStream = handler.getFileStream(context, zipfilename); - } else { - // manage zip via remote url - optionalFileStream = Optional.ofNullable(new URL(zipfilename).openStream()); - validationFileStream = Optional.ofNullable(new URL(zipfilename).openStream()); - } + try { + if (!remoteUrl) { + // manage zip via upload + optionalFileStream = handler.getFileStream(context, zipfilename); + validationFileStream = handler.getFileStream(context, zipfilename); + } else { + // manage zip via remote url + optionalFileStream = Optional.ofNullable(new URL(zipfilename).openStream()); + validationFileStream = Optional.ofNullable(new URL(zipfilename).openStream()); + } - if (validationFileStream.isPresent()) { - // validate zip file if (validationFileStream.isPresent()) { - validateZip(validationFileStream.get()); + // validate zip file + if (validationFileStream.isPresent()) { + validateZip(validationFileStream.get()); + } + + workFile = new File(itemImportService.getTempWorkDir() + File.separator + + zipfilename + "-" + context.getCurrentUser().getID()); + FileUtils.copyInputStreamToFile(optionalFileStream.get(), workFile); + } else { + throw new IllegalArgumentException( + "Error reading file, the file couldn't be found for filename: " + zipfilename); } - workFile = new File(itemImportService.getTempWorkDir() + File.separator - + zipfilename + "-" + context.getCurrentUser().getID()); - FileUtils.copyInputStreamToFile(optionalFileStream.get(), workFile); - } else { - throw new IllegalArgumentException( - "Error reading file, the file couldn't be found for filename: " + zipfilename); + workDir = new File(itemImportService.getTempWorkDir() + File.separator + TEMP_DIR + + File.separator + context.getCurrentUser().getID()); + sourcedir = itemImportService.unzip(workFile, workDir.getAbsolutePath()); + } finally { + optionalFileStream.ifPresent(IOUtils::closeQuietly); + validationFileStream.ifPresent(IOUtils::closeQuietly); } - - workDir = new File(itemImportService.getTempWorkDir() + File.separator + TEMP_DIR - + File.separator + context.getCurrentUser().getID()); - sourcedir = itemImportService.unzip(workFile, workDir.getAbsolutePath()); } /** diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportCLI.java b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportCLI.java index 98d2469b7155..bd29aa97fe48 100644 --- a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportCLI.java +++ b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportCLI.java @@ -17,6 +17,7 @@ import java.util.UUID; import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.dspace.app.itemimport.service.ItemImportService; import org.dspace.content.Collection; @@ -111,7 +112,11 @@ protected void readZip(Context context, ItemImportService itemImportService) thr // validate zip file InputStream validationFileStream = new FileInputStream(myZipFile); - validateZip(validationFileStream); + try { + validateZip(validationFileStream); + } finally { + IOUtils.closeQuietly(validationFileStream); + } workDir = new File(itemImportService.getTempWorkDir() + File.separator + TEMP_DIR + File.separator + context.getCurrentUser().getID()); @@ -120,22 +125,28 @@ protected void readZip(Context context, ItemImportService itemImportService) thr } else { // manage zip via remote url Optional optionalFileStream = Optional.ofNullable(new URL(zipfilename).openStream()); - if (optionalFileStream.isPresent()) { - // validate zip file via url - Optional validationFileStream = Optional.ofNullable(new URL(zipfilename).openStream()); - if (validationFileStream.isPresent()) { - validateZip(validationFileStream.get()); + Optional validationFileStream = Optional.ofNullable(new URL(zipfilename).openStream()); + try { + if (optionalFileStream.isPresent()) { + // validate zip file via url + + if (validationFileStream.isPresent()) { + validateZip(validationFileStream.get()); + } + + workFile = new File(itemImportService.getTempWorkDir() + File.separator + + zipfilename + "-" + context.getCurrentUser().getID()); + FileUtils.copyInputStreamToFile(optionalFileStream.get(), workFile); + workDir = new File(itemImportService.getTempWorkDir() + File.separator + TEMP_DIR + + File.separator + context.getCurrentUser().getID()); + sourcedir = itemImportService.unzip(workFile, workDir.getAbsolutePath()); + } else { + throw new IllegalArgumentException( + "Error reading file, the file couldn't be found for filename: " + zipfilename); } - - workFile = new File(itemImportService.getTempWorkDir() + File.separator - + zipfilename + "-" + context.getCurrentUser().getID()); - FileUtils.copyInputStreamToFile(optionalFileStream.get(), workFile); - workDir = new File(itemImportService.getTempWorkDir() + File.separator + TEMP_DIR - + File.separator + context.getCurrentUser().getID()); - sourcedir = itemImportService.unzip(workFile, workDir.getAbsolutePath()); - } else { - throw new IllegalArgumentException( - "Error reading file, the file couldn't be found for filename: " + zipfilename); + } finally { + optionalFileStream.ifPresent(IOUtils::closeQuietly); + validationFileStream.ifPresent(IOUtils::closeQuietly); } } } diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandedPreviewJPEGFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandedPreviewJPEGFilter.java index 7b082c6c21a4..483e4f5f6ea2 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandedPreviewJPEGFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandedPreviewJPEGFilter.java @@ -7,9 +7,7 @@ */ package org.dspace.app.mediafilter; -import java.awt.image.BufferedImage; import java.io.InputStream; -import javax.imageio.ImageIO; import org.dspace.content.Item; import org.dspace.services.ConfigurationService; @@ -63,27 +61,20 @@ public String getDescription() { @Override public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose) throws Exception { - // read in bitstream's image - BufferedImage buf = ImageIO.read(source); - // get config params ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); - float xmax = (float) configurationService - .getIntProperty("webui.preview.maxwidth"); - float ymax = (float) configurationService - .getIntProperty("webui.preview.maxheight"); - boolean blurring = (boolean) configurationService - .getBooleanProperty("webui.preview.blurring"); - boolean hqscaling = (boolean) configurationService - .getBooleanProperty("webui.preview.hqscaling"); + int xmax = configurationService.getIntProperty("webui.preview.maxwidth"); + int ymax = configurationService.getIntProperty("webui.preview.maxheight"); + boolean blurring = configurationService.getBooleanProperty("webui.preview.blurring"); + boolean hqscaling = configurationService.getBooleanProperty("webui.preview.hqscaling"); int brandHeight = configurationService.getIntProperty("webui.preview.brand.height"); String brandFont = configurationService.getProperty("webui.preview.brand.font"); int brandFontPoint = configurationService.getIntProperty("webui.preview.brand.fontpoint"); JPEGFilter jpegFilter = new JPEGFilter(); - return jpegFilter - .getThumbDim(currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, brandHeight, brandFontPoint, - brandFont); + return jpegFilter.getThumb( + currentItem, source, verbose, xmax, ymax, blurring, hqscaling, brandHeight, brandFontPoint, brandFont + ); } } diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickThumbnailFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickThumbnailFilter.java index 7543410a7968..28bfc72dc110 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickThumbnailFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickThumbnailFilter.java @@ -105,7 +105,7 @@ public File getThumbnailFile(File f, boolean verbose) ConvertCmd cmd = new ConvertCmd(); IMOperation op = new IMOperation(); op.autoOrient(); - op.addImage(f.getAbsolutePath()); + op.addImage(f.getAbsolutePath() + "[0]"); op.thumbnail(configurationService.getIntProperty("thumbnail.maxwidth", DEFAULT_WIDTH), configurationService.getIntProperty("thumbnail.maxheight", DEFAULT_HEIGHT)); op.addImage(f2.getAbsolutePath()); diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java index 502f71eb5ca8..2ccc2afbb2d2 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java @@ -8,19 +8,32 @@ package org.dspace.app.mediafilter; import java.awt.Color; +import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Transparency; +import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.awt.image.BufferedImageOp; import java.awt.image.ConvolveOp; import java.awt.image.Kernel; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStream; import javax.imageio.ImageIO; +import com.drew.imaging.ImageMetadataReader; +import com.drew.imaging.ImageProcessingException; +import com.drew.metadata.Metadata; +import com.drew.metadata.MetadataException; +import com.drew.metadata.exif.ExifIFD0Directory; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.Item; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; @@ -33,6 +46,8 @@ * @author Jason Sherman jsherman@usao.edu */ public class JPEGFilter extends MediaFilter implements SelfRegisterInputFormats { + private static final Logger log = LogManager.getLogger(JPEGFilter.class); + @Override public String getFilteredName(String oldFilename) { return oldFilename + ".jpg"; @@ -62,6 +77,115 @@ public String getDescription() { return "Generated Thumbnail"; } + /** + * Gets the rotation angle from image's metadata using ImageReader. + * This method consumes the InputStream, so you need to be careful to don't reuse the same InputStream after + * computing the rotation angle. + * + * @param buf InputStream of the image file + * @return Rotation angle in degrees (0, 90, 180, or 270) + */ + public static int getImageRotationUsingImageReader(InputStream buf) { + try { + Metadata metadata = ImageMetadataReader.readMetadata(buf); + ExifIFD0Directory directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); + if (directory != null && directory.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) { + return convertRotationToDegrees(directory.getInt(ExifIFD0Directory.TAG_ORIENTATION)); + } + } catch (MetadataException | ImageProcessingException | IOException e) { + log.error("Error reading image metadata", e); + } + return 0; + } + + public static int convertRotationToDegrees(int valueNode) { + // Common orientation values: + // 1 = Normal (0°) + // 6 = Rotated 90° CW + // 3 = Rotated 180° + // 8 = Rotated 270° CW + switch (valueNode) { + case 6: + return 90; + case 3: + return 180; + case 8: + return 270; + default: + return 0; + } + } + + /** + * Rotates an image by the specified angle + * + * @param image The original image + * @param angle The rotation angle in degrees + * @return Rotated image + */ + public static BufferedImage rotateImage(BufferedImage image, int angle) { + if (angle == 0) { + return image; + } + + double radians = Math.toRadians(angle); + double sin = Math.abs(Math.sin(radians)); + double cos = Math.abs(Math.cos(radians)); + + int newWidth = (int) Math.round(image.getWidth() * cos + image.getHeight() * sin); + int newHeight = (int) Math.round(image.getWidth() * sin + image.getHeight() * cos); + + BufferedImage rotated = new BufferedImage(newWidth, newHeight, image.getType()); + Graphics2D g2d = rotated.createGraphics(); + AffineTransform at = new AffineTransform(); + + at.translate(newWidth / 2, newHeight / 2); + at.rotate(radians); + at.translate(-image.getWidth() / 2, -image.getHeight() / 2); + + g2d.setTransform(at); + g2d.drawImage(image, 0, 0, null); + g2d.dispose(); + + return rotated; + } + + /** + * Calculates scaled dimension while maintaining aspect ratio + * + * @param imgSize Original image dimensions + * @param boundary Maximum allowed dimensions + * @return New dimensions that fit within boundary while preserving aspect ratio + */ + private Dimension getScaledDimension(Dimension imgSize, Dimension boundary) { + + int originalWidth = imgSize.width; + int originalHeight = imgSize.height; + int boundWidth = boundary.width; + int boundHeight = boundary.height; + int newWidth = originalWidth; + int newHeight = originalHeight; + + + // First check if we need to scale width + if (originalWidth > boundWidth) { + // Scale width to fit + newWidth = boundWidth; + // Scale height to maintain aspect ratio + newHeight = (newWidth * originalHeight) / originalWidth; + } + + // Then check if we need to scale even with the new height + if (newHeight > boundHeight) { + // Scale height to fit instead + newHeight = boundHeight; + newWidth = (newHeight * originalWidth) / originalHeight; + } + + return new Dimension(newWidth, newHeight); + } + + /** * @param currentItem item * @param source source input stream @@ -72,10 +196,65 @@ public String getDescription() { @Override public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose) throws Exception { - // read in bitstream's image - BufferedImage buf = ImageIO.read(source); + return getThumb(currentItem, source, verbose); + } - return getThumb(currentItem, buf, verbose); + public InputStream getThumb(Item currentItem, InputStream source, boolean verbose) + throws Exception { + // get config params + final ConfigurationService configurationService + = DSpaceServicesFactory.getInstance().getConfigurationService(); + int xmax = configurationService + .getIntProperty("thumbnail.maxwidth"); + int ymax = configurationService + .getIntProperty("thumbnail.maxheight"); + boolean blurring = (boolean) configurationService + .getBooleanProperty("thumbnail.blurring"); + boolean hqscaling = (boolean) configurationService + .getBooleanProperty("thumbnail.hqscaling"); + + return getThumb(currentItem, source, verbose, xmax, ymax, blurring, hqscaling, 0, 0, null); + } + + protected InputStream getThumb( + Item currentItem, + InputStream source, + boolean verbose, + int xmax, + int ymax, + boolean blurring, + boolean hqscaling, + int brandHeight, + int brandFontPoint, + String brandFont + ) throws Exception { + + File tempFile = File.createTempFile("temp", ".tmp"); + tempFile.deleteOnExit(); + + // Write to temp file + try (FileOutputStream fos = new FileOutputStream(tempFile)) { + byte[] buffer = new byte[4096]; + int len; + while ((len = source.read(buffer)) != -1) { + fos.write(buffer, 0, len); + } + } + + int rotation = 0; + try (FileInputStream fis = new FileInputStream(tempFile)) { + rotation = getImageRotationUsingImageReader(fis); + } + + try (FileInputStream fis = new FileInputStream(tempFile)) { + // read in bitstream's image + BufferedImage buf = ImageIO.read(fis); + + return getThumbDim( + currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, brandHeight, brandFontPoint, rotation, + brandFont + ); + } } public InputStream getThumb(Item currentItem, BufferedImage buf, boolean verbose) @@ -83,25 +262,28 @@ public InputStream getThumb(Item currentItem, BufferedImage buf, boolean verbose // get config params final ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); - float xmax = (float) configurationService + int xmax = configurationService .getIntProperty("thumbnail.maxwidth"); - float ymax = (float) configurationService + int ymax = configurationService .getIntProperty("thumbnail.maxheight"); boolean blurring = (boolean) configurationService .getBooleanProperty("thumbnail.blurring"); boolean hqscaling = (boolean) configurationService .getBooleanProperty("thumbnail.hqscaling"); - return getThumbDim(currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, 0, 0, null); + return getThumbDim(currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, 0, 0, 0, null); } - public InputStream getThumbDim(Item currentItem, BufferedImage buf, boolean verbose, float xmax, float ymax, + public InputStream getThumbDim(Item currentItem, BufferedImage buf, boolean verbose, int xmax, int ymax, boolean blurring, boolean hqscaling, int brandHeight, int brandFontPoint, - String brandFont) + int rotation, String brandFont) throws Exception { - // now get the image dimensions - float xsize = (float) buf.getWidth(null); - float ysize = (float) buf.getHeight(null); + + // Rotate the image if needed + BufferedImage correctedImage = rotateImage(buf, rotation); + + int xsize = correctedImage.getWidth(); + int ysize = correctedImage.getHeight(); // if verbose flag is set, print out dimensions // to STDOUT @@ -109,86 +291,63 @@ public InputStream getThumbDim(Item currentItem, BufferedImage buf, boolean verb System.out.println("original size: " + xsize + "," + ysize); } - // scale by x first if needed - if (xsize > xmax) { - // calculate scaling factor so that xsize * scale = new size (max) - float scale_factor = xmax / xsize; + // Calculate new dimensions while maintaining aspect ratio + Dimension newDimension = getScaledDimension( + new Dimension(xsize, ysize), + new Dimension(xmax, ymax) + ); - // if verbose flag is set, print out extracted text - // to STDOUT - if (verbose) { - System.out.println("x scale factor: " + scale_factor); - } - - // now reduce x size - // and y size - xsize = xsize * scale_factor; - ysize = ysize * scale_factor; - - // if verbose flag is set, print out extracted text - // to STDOUT - if (verbose) { - System.out.println("size after fitting to maximum width: " + xsize + "," + ysize); - } - } - - // scale by y if needed - if (ysize > ymax) { - float scale_factor = ymax / ysize; - - // now reduce x size - // and y size - xsize = xsize * scale_factor; - ysize = ysize * scale_factor; - } // if verbose flag is set, print details to STDOUT if (verbose) { - System.out.println("size after fitting to maximum height: " + xsize + ", " - + ysize); + System.out.println("size after fitting to maximum height: " + newDimension.width + ", " + + newDimension.height); } + xsize = newDimension.width; + ysize = newDimension.height; + // create an image buffer for the thumbnail with the new xsize, ysize - BufferedImage thumbnail = new BufferedImage((int) xsize, (int) ysize, - BufferedImage.TYPE_INT_RGB); + BufferedImage thumbnail = new BufferedImage(xsize, ysize, BufferedImage.TYPE_INT_RGB); // Use blurring if selected in config. // a little blur before scaling does wonders for keeping moire in check. if (blurring) { // send the buffered image off to get blurred. - buf = getBlurredInstance((BufferedImage) buf); + correctedImage = getBlurredInstance(correctedImage); } // Use high quality scaling method if selected in config. // this has a definite performance penalty. if (hqscaling) { // send the buffered image off to get an HQ downscale. - buf = getScaledInstance((BufferedImage) buf, (int) xsize, (int) ysize, - (Object) RenderingHints.VALUE_INTERPOLATION_BICUBIC, (boolean) true); + correctedImage = getScaledInstance(correctedImage, xsize, ysize, + RenderingHints.VALUE_INTERPOLATION_BICUBIC, true); } // now render the image into the thumbnail buffer Graphics2D g2d = thumbnail.createGraphics(); - g2d.drawImage(buf, 0, 0, (int) xsize, (int) ysize, null); + g2d.drawImage(correctedImage, 0, 0, xsize, ysize, null); if (brandHeight != 0) { ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); - Brand brand = new Brand((int) xsize, brandHeight, new Font(brandFont, Font.PLAIN, brandFontPoint), 5); + Brand brand = new Brand(xsize, brandHeight, new Font(brandFont, Font.PLAIN, brandFontPoint), 5); BufferedImage brandImage = brand.create(configurationService.getProperty("webui.preview.brand"), configurationService.getProperty("webui.preview.brand.abbrev"), currentItem == null ? "" : "hdl:" + currentItem.getHandle()); - g2d.drawImage(brandImage, (int) 0, (int) ysize, (int) xsize, (int) 20, null); + g2d.drawImage(brandImage, 0, ysize, xsize, 20, null); } - // now create an input stream for the thumbnail buffer and return it - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - ImageIO.write(thumbnail, "jpeg", baos); - // now get the array - ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ByteArrayInputStream bais; + // now create an input stream for the thumbnail buffer and return it + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + ImageIO.write(thumbnail, "jpeg", baos); + // now get the array + bais = new ByteArrayInputStream(baos.toByteArray()); + } return bais; // hope this gets written out before its garbage collected! } diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java index 94c463b2808f..eb23e9daa085 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java @@ -83,6 +83,7 @@ public InputStream getDestinationStream(Item currentItem, InputStream source, bo // Generate thumbnail derivative and return as IO stream. JPEGFilter jpegFilter = new JPEGFilter(); + return jpegFilter.getThumb(currentItem, buf, verbose); } } diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemEmailNotifier.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemEmailNotifier.java index c489fb4b3ff0..49b82210dc23 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemEmailNotifier.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemEmailNotifier.java @@ -9,7 +9,9 @@ package org.dspace.app.requestitem; import java.io.IOException; +import java.io.InputStream; import java.sql.SQLException; +import java.util.ArrayList; import java.util.List; import jakarta.annotation.ManagedBean; @@ -186,6 +188,7 @@ public void sendResponse(Context context, RequestItem ri, String subject, email.setSubject(subject); email.addRecipient(ri.getReqEmail()); // Attach bitstreams. + List bitstreamInputStreams = new ArrayList<>(); try { if (ri.isAccept_request()) { if (ri.isAllfiles()) { @@ -200,11 +203,13 @@ public void sendResponse(Context context, RequestItem ri, String subject, // #8636 Anyone receiving the email can respond to the // request without authenticating into DSpace context.turnOffAuthorisationSystem(); + InputStream is = bitstreamService.retrieve(context, bitstream); email.addAttachment( - bitstreamService.retrieve(context, bitstream), + is, bitstream.getName(), bitstream.getFormat(context).getMIMEType()); context.restoreAuthSystemState(); + bitstreamInputStreams.add(is); } } } @@ -212,10 +217,12 @@ public void sendResponse(Context context, RequestItem ri, String subject, Bitstream bitstream = ri.getBitstream(); // #8636 Anyone receiving the email can respond to the request without authenticating into DSpace context.turnOffAuthorisationSystem(); - email.addAttachment(bitstreamService.retrieve(context, bitstream), + InputStream is = bitstreamService.retrieve(context, bitstream); + email.addAttachment(is, bitstream.getName(), bitstream.getFormat(context).getMIMEType()); context.restoreAuthSystemState(); + bitstreamInputStreams.add(is); } email.send(); } else { @@ -231,6 +238,10 @@ public void sendResponse(Context context, RequestItem ri, String subject, LOG.warn(LogHelper.getHeader(context, "error_mailing_requestItem", e.getMessage())); throw new IOException("Reply not sent: " + e.getMessage()); + } finally { + for (InputStream bitstreamInputStream : bitstreamInputStreams) { + bitstreamInputStream.close(); + } } LOG.info(LogHelper.getHeader(context, "sent_attach_requestItem", "token={}"), ri.getToken()); diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemServiceImpl.java index b915cfedd346..d6d0225655e0 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemServiceImpl.java @@ -11,6 +11,7 @@ import java.util.Date; import java.util.Iterator; import java.util.List; +import java.util.UUID; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -96,6 +97,11 @@ public Iterator findByItem(Context context, Item item) throws SQLEx return requestItemDAO.findByItem(context, item); } + @Override + public Iterator findByBitstreamId(Context context, UUID bitstreamId) throws SQLException { + return requestItemDAO.findByBitstreamId(context, bitstreamId); + } + @Override public void update(Context context, RequestItem requestItem) { try { diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/dao/RequestItemDAO.java b/dspace-api/src/main/java/org/dspace/app/requestitem/dao/RequestItemDAO.java index b36ae58e0ca1..9c6954fe6be1 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/dao/RequestItemDAO.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/dao/RequestItemDAO.java @@ -9,6 +9,7 @@ import java.sql.SQLException; import java.util.Iterator; +import java.util.UUID; import org.dspace.app.requestitem.RequestItem; import org.dspace.content.Item; @@ -26,7 +27,7 @@ */ public interface RequestItemDAO extends GenericDAO { /** - * Fetch a request named by its unique token (passed in emails). + * Fetch a request named by its unique approval token (passed in emails). * * @param context the current DSpace context. * @param token uniquely identifies the request. @@ -36,4 +37,17 @@ public interface RequestItemDAO extends GenericDAO { public RequestItem findByToken(Context context, String token) throws SQLException; public Iterator findByItem(Context context, Item item) throws SQLException; + + /** + * Retrieve all requests (as iterator) for a given bitstream UUID + * A UUID parameter is used here rather than Bitstream object, to make it usable + * in situations even when a bitstream object no longer exists, but orphaned + * entries need to be found by their (previous) bitstream UUID. + * + * @param context current DSpace context + * @param bitstreamId the bitstream UUID to search for + * @return the matching requests (or empty iterator) + */ + public Iterator findByBitstreamId(Context context, UUID bitstreamId) throws SQLException; + } diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/dao/impl/RequestItemDAOImpl.java b/dspace-api/src/main/java/org/dspace/app/requestitem/dao/impl/RequestItemDAOImpl.java index c76bd50d1910..d6c6c1b231b7 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/dao/impl/RequestItemDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/dao/impl/RequestItemDAOImpl.java @@ -9,6 +9,7 @@ import java.sql.SQLException; import java.util.Iterator; +import java.util.UUID; import jakarta.persistence.Query; import jakarta.persistence.criteria.CriteriaBuilder; @@ -52,4 +53,11 @@ public Iterator findByItem(Context context, Item item) throws SQLEx Query query = createQuery(context, criteriaQuery); return iterate(query); } + + @Override + public Iterator findByBitstreamId(Context context, UUID bitstreamId) throws SQLException { + Query query = createQuery(context, "FROM RequestItem WHERE bitstream.id = :bitstreamId"); + query.setParameter("bitstreamId", bitstreamId); + return iterate(query); + } } diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/service/RequestItemService.java b/dspace-api/src/main/java/org/dspace/app/requestitem/service/RequestItemService.java index efac3b18bc7c..5c27ba9acb55 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/service/RequestItemService.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/service/RequestItemService.java @@ -10,6 +10,7 @@ import java.sql.SQLException; import java.util.Iterator; import java.util.List; +import java.util.UUID; import org.dspace.app.requestitem.RequestItem; import org.dspace.content.Bitstream; @@ -40,7 +41,7 @@ public interface RequestItemService { * @return the token of the request item * @throws SQLException if database error */ - public String createRequest(Context context, Bitstream bitstream, Item item, + String createRequest(Context context, Bitstream bitstream, Item item, boolean allFiles, String reqEmail, String reqName, String reqMessage) throws SQLException; @@ -49,35 +50,51 @@ public String createRequest(Context context, Bitstream bitstream, Item item, * * @param context current DSpace session. * @return all item requests. - * @throws java.sql.SQLException passed through. + * @throws SQLException passed through. */ - public List findAll(Context context) + List findAll(Context context) throws SQLException; /** - * Retrieve a request by its token. + * Retrieve a request by its approver token. * * @param context current DSpace session. - * @param token the token identifying the request. + * @param token the token identifying the request to be approved. * @return the matching request, or null if not found. */ - public RequestItem findByToken(Context context, String token); + RequestItem findByToken(Context context, String token); /** - * Retrieve a request based on the item. + * Retrieve all requests (as iterator) for a given item * @param context current DSpace session. * @param item the item to find requests for. - * @return the matching requests, or null if not found. + * @return the matching requests (or empty iterator) */ - public Iterator findByItem(Context context, Item item) throws SQLException; + Iterator findByItem(Context context, Item item) throws SQLException; + /** - * Save updates to the record. Only accept_request, and decision_date are set-able. + * Retrieve all requests (as iterator) for a given bitstream UUID + * A UUID parameter is used here rather than Bitstream object, to make it usable + * in situations even when a bitstream object no longer exists, but orphaned + * entries need to be found by their (previous) bitstream UUID. + * + * @param context current DSpace context + * @param bitstreamId the bitstream UUID to search for + * @return the matching requests (or empty iterator) + */ + Iterator findByBitstreamId(Context context, UUID bitstreamId) throws SQLException; + + /** + * Save updates to the record. Only accept_request, decision_date, access_period are settable. + * + * Note: the "is settable" rules mentioned here are enforced in RequestItemRest with annotations meaning that + * these JSON properties are considered READ-ONLY by the core DSpaceRestRepository methods * * @param context The relevant DSpace Context. * @param requestItem requested item */ - public void update(Context context, RequestItem requestItem); + void update(Context context, RequestItem requestItem); /** * Remove the record from the database. @@ -85,7 +102,7 @@ public List findAll(Context context) * @param context current DSpace context. * @param request record to be removed. */ - public void delete(Context context, RequestItem request); + void delete(Context context, RequestItem request); /** * Is there at least one valid READ resource policy for this object? @@ -94,6 +111,6 @@ public List findAll(Context context) * @return true if a READ policy applies. * @throws SQLException passed through. */ - public boolean isRestricted(Context context, DSpaceObject o) + boolean isRestricted(Context context, DSpaceObject o) throws SQLException; } diff --git a/dspace-api/src/main/java/org/dspace/app/util/DSpaceObjectUtilsImpl.java b/dspace-api/src/main/java/org/dspace/app/util/DSpaceObjectUtilsImpl.java index e3f2b0ea5faa..33621abd529d 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/DSpaceObjectUtilsImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/util/DSpaceObjectUtilsImpl.java @@ -15,12 +15,15 @@ import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.DSpaceObjectService; import org.dspace.core.Context; +import org.dspace.handle.service.HandleService; import org.springframework.beans.factory.annotation.Autowired; public class DSpaceObjectUtilsImpl implements DSpaceObjectUtils { @Autowired private ContentServiceFactory contentServiceFactory; + @Autowired + private HandleService handleService; /** * Retrieve a DSpaceObject from its uuid. As this method need to iterate over all the different services that @@ -44,4 +47,32 @@ public DSpaceObject findDSpaceObject(Context context, UUID uuid) throws SQLExcep } return null; } + + /** + * Retrieve a DSpaceObject from its uuid or handle. As this method need to iterate over all the different services + * that support concrete class of DSpaceObject it has poor performance. Please consider the use of the direct + * service (ItemService, CommunityService, etc.) if you know in advance the type of DSpaceObject that you are + * looking for + * + * @param context DSpace context + * @param id the uuid or handle to lookup + * @return the DSpaceObject if any with the supplied uuid or handle + * @throws SQLException + */ + public DSpaceObject findDSpaceObject(Context context, String id) throws SQLException { + DSpaceObject dso = handleService.resolveToObject(context, id); + // if the id did not resolve to a handle, check if it is a uuid + if (dso == null) { + UUID uuid = null; + try { + uuid = UUID.fromString(id); + } catch (IllegalArgumentException iae) { + // nothing to do here. We check later fo empty uuids anyway + } + if (uuid != null) { + dso = findDSpaceObject(context, uuid); + } + } + return dso; + } } diff --git a/dspace-api/src/main/java/org/dspace/app/util/MetadataExposureServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/util/MetadataExposureServiceImpl.java index 681867371a06..9a281d65775b 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/MetadataExposureServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/util/MetadataExposureServiceImpl.java @@ -63,7 +63,7 @@ public class MetadataExposureServiceImpl implements MetadataExposureService { protected Map> hiddenElementSets = null; protected Map>> hiddenElementMaps = null; - protected final String CONFIG_PREFIX = "metadata.hide."; + public static final String CONFIG_PREFIX = "metadata.hide."; @Autowired(required = true) protected AuthorizeService authorizeService; diff --git a/dspace-api/src/main/java/org/dspace/app/util/service/DSpaceObjectUtils.java b/dspace-api/src/main/java/org/dspace/app/util/service/DSpaceObjectUtils.java index e6a97004ef60..8088a4ca4de5 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/service/DSpaceObjectUtils.java +++ b/dspace-api/src/main/java/org/dspace/app/util/service/DSpaceObjectUtils.java @@ -30,4 +30,17 @@ public interface DSpaceObjectUtils { * @throws SQLException */ public DSpaceObject findDSpaceObject(Context context, UUID uuid) throws SQLException; + + /** + * Retrieve a DSpaceObject from its uuid or handle. As this method need to iterate over all the different services + * that support concrete class of DSpaceObject it has poor performance. Please consider the use of the direct + * service (ItemService, CommunityService, etc.) if you know in advance the type of DSpaceObject that you are + * looking for + * + * @param context DSpace context + * @param id the uuid or handle to lookup + * @return the DSpaceObject if any with the supplied uuid or handle + * @throws SQLException + */ + public DSpaceObject findDSpaceObject(Context context, String id) throws SQLException; } diff --git a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java index d316cb636f87..a7140f244dfa 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java @@ -54,7 +54,7 @@ public interface AuthenticationMethod { public static final int BAD_CREDENTIALS = 2; /** - * Not allowed to login this way without X.509 certificate. + * Not allowed to login this way without a certificate. */ public static final int CERT_REQUIRED = 3; @@ -124,8 +124,8 @@ public boolean allowSetPassword(Context context, * Predicate, is this an implicit authentication method. * An implicit method gets credentials from the environment (such as * an HTTP request or even Java system properties) rather than the - * explicit username and password. For example, a method that reads - * the X.509 certificates in an HTTPS request is implicit. + * explicit username and password. For example, a method that provides + * IP-based authentication is implicit. * * @return true if this method uses implicit authentication. */ @@ -166,7 +166,7 @@ public List getSpecialGroups(Context context, HttpServletRequest request) * otherwise */ public default boolean areSpecialGroupsApplicable(Context context, HttpServletRequest request) { - return getName().equals(context.getAuthenticationMethod()); + return getName().equals(context.getAuthenticationMethod()) || isUsed(context, request); } /** @@ -188,7 +188,7 @@ public default boolean areSpecialGroupsApplicable(Context context, HttpServletRe *

Meaning: *
SUCCESS - authenticated OK. *
BAD_CREDENTIALS - user exists, but credentials (e.g. passwd) don't match - *
CERT_REQUIRED - not allowed to login this way without X.509 cert. + *
CERT_REQUIRED - not allowed to login this way without a cert. *
NO_SUCH_USER - user not found using this method. *
BAD_ARGS - user/pw not appropriate for this method * @throws SQLException if database error diff --git a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java index 2b07f73c489c..c0dde49b13a3 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java @@ -38,11 +38,11 @@ * Configuration
* The stack of authentication methods is defined by one property in the DSpace configuration: *

- *   plugin.sequence.org.dspace.eperson.AuthenticationMethod = a list of method class names
+ *   plugin.sequence.org.dspace.authenticate.AuthenticationMethod = a list of method class names
  *     e.g.
- *   plugin.sequence.org.dspace.eperson.AuthenticationMethod = \
- *       org.dspace.eperson.X509Authentication, \
- *       org.dspace.eperson.PasswordAuthentication
+ *   plugin.sequence.org.dspace.authenticate.AuthenticationMethod = \
+ *       org.dspace.authenticate.IPAuthentication, \
+ *       org.dspace.authenticate.PasswordAuthentication
  * 
*

* The "stack" is always traversed in order, with the methods @@ -110,6 +110,7 @@ protected int authenticateInternal(Context context, } if (ret == AuthenticationMethod.SUCCESS) { updateLastActiveDate(context); + context.setAuthenticationMethod(aMethodStack.getName()); return ret; } if (ret < bestRet) { 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 40b8f48078c9..b199cdb94685 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java @@ -9,27 +9,13 @@ import static org.dspace.eperson.service.EPersonService.MD_PHONE; -import java.io.IOException; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Optional; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.BasicAttribute; -import javax.naming.directory.BasicAttributes; -import javax.naming.directory.SearchControls; -import javax.naming.directory.SearchResult; -import javax.naming.ldap.InitialLdapContext; -import javax.naming.ldap.LdapContext; -import javax.naming.ldap.StartTlsRequest; -import javax.naming.ldap.StartTlsResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -47,6 +33,16 @@ import org.dspace.eperson.service.GroupService; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; +import org.springframework.ldap.core.ContextMapper; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.ldap.core.support.LdapContextSource; +import org.springframework.ldap.filter.EqualsFilter; +import org.springframework.ldap.filter.PresentFilter; +import org.springframework.ldap.query.LdapQueryBuilder; +import org.springframework.ldap.support.LdapNameBuilder; +import org.springframework.ldap.support.LdapUtils; + /** * This combined LDAP authentication method supersedes both the 'LDAPAuthentication' @@ -203,7 +199,7 @@ public List getSpecialGroups(Context context, HttpServletRequest request) *

Meaning: *
SUCCESS - authenticated OK. *
BAD_CREDENTIALS - user exists, but credentials (e.g. passwd) don't match - *
CERT_REQUIRED - not allowed to login this way without X.509 cert. + *
CERT_REQUIRED - not allowed to login this way without a cert. *
NO_SUCH_USER - user not found using this method. *
BAD_ARGS - user/pw not appropriate for this method */ @@ -238,12 +234,18 @@ public int authenticate(Context context, String idField = configurationService.getProperty("authentication-ldap.id_field"); String dn = ""; - // If adminUser is blank and anonymous search is not allowed, then we can't search so construct the DN - // instead of searching it if ((StringUtils.isBlank(adminUser) || StringUtils.isBlank(adminPassword)) && !anonymousSearch) { - dn = idField + "=" + netid + "," + objectContext; + try { + dn = LdapNameBuilder.newInstance(objectContext) + .add(idField, netid) + .build() + .toString(); + } catch (Exception e) { + log.warn("Failed to build DN for user " + netid, e); + return BAD_ARGS; + } } else { - dn = ldap.getDNOfUser(adminUser, adminPassword, context, netid); + dn = ldap.getDNOfUser(context, netid); } // Check a DN was found @@ -322,7 +324,7 @@ public int authenticate(Context context, log.info(LogHelper.getHeader(context, "type=ldap-login", "type=ldap_but_already_email")); context.turnOffAuthorisationSystem(); - setEpersonAttributes(context, eperson, ldap, Optional.of(netid)); + setEpersonAttributes(context, eperson, ldap, Optional.of(netid), email); ePersonService.update(context, eperson); context.dispatchEvents(); context.restoreAuthSystemState(); @@ -339,7 +341,7 @@ public int authenticate(Context context, try { context.turnOffAuthorisationSystem(); eperson = ePersonService.create(context); - setEpersonAttributes(context, eperson, ldap, Optional.of(netid)); + setEpersonAttributes(context, eperson, ldap, Optional.of(netid), email); eperson.setCanLogIn(true); authenticationService.initEPerson(context, request, eperson); ePersonService.update(context, eperson); @@ -381,11 +383,24 @@ public int authenticate(Context context, * Update eperson's attributes */ private void setEpersonAttributes(Context context, EPerson eperson, SpeakerToLDAP ldap, Optional netid) - throws SQLException { + throws SQLException { + setEpersonAttributes(context, eperson, ldap, netid, null); + } + + /** + * Update eperson's attributes + */ + private void setEpersonAttributes(Context context, EPerson eperson, SpeakerToLDAP ldap, Optional netid, + String email) + throws SQLException { + // Set email address: first try LDAP email, then fallback to provided email parameter if (StringUtils.isNotEmpty(ldap.ldapEmail)) { eperson.setEmail(ldap.ldapEmail); + } else if (StringUtils.isNotEmpty(email)) { + eperson.setEmail(email); } + if (StringUtils.isNotEmpty(ldap.ldapGivenName)) { eperson.setFirstName(context, ldap.ldapGivenName); } @@ -429,6 +444,7 @@ private static class SpeakerToLDAP { final String ldap_group_field; final boolean useTLS; + private LdapTemplate ldapTemplate; SpeakerToLDAP(Logger thelog) { ConfigurationService configurationService @@ -445,252 +461,112 @@ private static class SpeakerToLDAP { ldap_phone_field = configurationService.getProperty("authentication-ldap.phone_field"); ldap_group_field = configurationService.getProperty("authentication-ldap.login.groupmap.attribute"); useTLS = configurationService.getBooleanProperty("authentication-ldap.starttls", false); - } - protected String getDNOfUser(String adminUser, String adminPassword, Context context, String netid) { - // The resultant DN - String resultDN; + setupSpringLdap(configurationService); + } - // The search scope to use (default to 0) - int ldap_search_scope_value = 0; - try { - ldap_search_scope_value = Integer.parseInt(ldap_search_scope.trim()); - } catch (NumberFormatException e) { - // Log the error if it has been set but is invalid - if (ldap_search_scope != null) { - log.warn(LogHelper.getHeader(context, - "ldap_authentication", "invalid search scope: " + ldap_search_scope)); - } + private void setupSpringLdap(ConfigurationService cfg) { + LdapContextSource contextSource = new LdapContextSource(); + if (StringUtils.isBlank(ldap_provider_url)) { + throw new IllegalStateException( + "LDAP provider URL is empty! Please check 'authentication-ldap.provider_url' in your configuration." + ); } + contextSource.setUrl(ldap_provider_url); - // Set up environment for creating initial context - @SuppressWarnings("UseOfObsoleteCollectionType") - Hashtable env = new Hashtable<>(); - env.put(javax.naming.Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); - env.put(javax.naming.Context.PROVIDER_URL, ldap_provider_url); - - LdapContext ctx = null; - StartTlsResponse startTLSResponse = null; + String adminUser = cfg.getProperty("authentication-ldap.search.user"); + String adminPass = cfg.getProperty("authentication-ldap.search.password"); - try { - if ((adminUser != null) && (!adminUser.trim().equals("")) && - (adminPassword != null) && (!adminPassword.trim().equals(""))) { - if (useTLS) { - ctx = new InitialLdapContext(env, null); - // start TLS - startTLSResponse = (StartTlsResponse) ctx - .extendedOperation(new StartTlsRequest()); - - startTLSResponse.negotiate(); - - // perform simple client authentication - ctx.addToEnvironment(javax.naming.Context.SECURITY_AUTHENTICATION, "simple"); - ctx.addToEnvironment(javax.naming.Context.SECURITY_PRINCIPAL, - adminUser); - ctx.addToEnvironment(javax.naming.Context.SECURITY_CREDENTIALS, - adminPassword); - } else { - // Use admin credentials for search// Authenticate - env.put(javax.naming.Context.SECURITY_AUTHENTICATION, "simple"); - env.put(javax.naming.Context.SECURITY_PRINCIPAL, adminUser); - env.put(javax.naming.Context.SECURITY_CREDENTIALS, adminPassword); - } - } else { - // Use anonymous authentication - env.put(javax.naming.Context.SECURITY_AUTHENTICATION, "none"); - } + if (StringUtils.isNotBlank(adminUser) && StringUtils.isNotBlank(adminPass)) { + contextSource.setUserDn(adminUser); + contextSource.setPassword(adminPass); + } else { + contextSource.setAnonymousReadOnly(true); + } - if (ctx == null) { - // Create initial context - ctx = new InitialLdapContext(env, null); - } + contextSource.setPooled(true); + contextSource.afterPropertiesSet(); + this.ldapTemplate = new LdapTemplate(contextSource); + this.ldapTemplate.setIgnorePartialResultException(true); + } - Attributes matchAttrs = new BasicAttributes(true); - matchAttrs.put(new BasicAttribute(ldap_id_field, netid)); + protected String getDNOfUser(Context context, String netid) { + try { + EqualsFilter filter = new EqualsFilter(ldap_id_field, netid); - // look up attributes - try { - SearchControls ctrls = new SearchControls(); - ctrls.setSearchScope(ldap_search_scope_value); - // Fetch both user attributes '*' (eg. uid, cn) and operational attributes '+' (eg. memberOf) - ctrls.setReturningAttributes(new String[] {"*", "+"}); - - String searchName; - if (useTLS) { - searchName = ldap_search_context; - } else { - searchName = ldap_provider_url + ldap_search_context; - } - @SuppressWarnings("BanJNDI") - NamingEnumeration answer = ctx.search( - searchName, - "(&({0}={1}))", new Object[] {ldap_id_field, - netid}, ctrls); - - while (answer.hasMoreElements()) { - SearchResult sr = answer.next(); - if (StringUtils.isEmpty(ldap_search_context)) { - resultDN = sr.getName(); - } else { - resultDN = (sr.getName() + "," + ldap_search_context); - } + log.debug("Searching for user using Spring LDAP filter: {}", filter.toString()); - String attlist[] = {ldap_email_field, ldap_givenname_field, - ldap_surname_field, ldap_phone_field, ldap_group_field}; - Attributes atts = sr.getAttributes(); - Attribute att; + List foundDNs = ldapTemplate.search( + LdapQueryBuilder.query().base(ldap_search_context).filter(filter), + (ContextMapper) (originalCtx) -> { + DirContextOperations ctx = (DirContextOperations) originalCtx; - if (attlist[0] != null) { - att = atts.get(attlist[0]); - if (att != null) { - ldapEmail = (String) att.get(); - } + if (ldap_email_field != null) { + this.ldapEmail = ctx.getStringAttribute(ldap_email_field); } - if (attlist[1] != null) { - att = atts.get(attlist[1]); - if (att != null) { - ldapGivenName = (String) att.get(); - } + if (ldap_givenname_field != null) { + this.ldapGivenName = ctx.getStringAttribute(ldap_givenname_field); } - if (attlist[2] != null) { - att = atts.get(attlist[2]); - if (att != null) { - ldapSurname = (String) att.get(); - } + if (ldap_surname_field != null) { + this.ldapSurname = ctx.getStringAttribute(ldap_surname_field); } - if (attlist[3] != null) { - att = atts.get(attlist[3]); - if (att != null) { - ldapPhone = (String) att.get(); - } + if (ldap_phone_field != null) { + this.ldapPhone = ctx.getStringAttribute(ldap_phone_field); } - if (attlist[4] != null) { - att = atts.get(attlist[4]); - if (att != null) { - // loop through all groups returned by LDAP - ldapGroup = new ArrayList<>(); - for (NamingEnumeration val = att.getAll(); val.hasMoreElements(); ) { - ldapGroup.add((String) val.next()); - } + if (ldap_group_field != null) { + String[] groups = ctx.getStringAttributes(ldap_group_field); + if (groups != null) { + this.ldapGroup = new ArrayList<>(Arrays.asList(groups)); } } - - if (answer.hasMoreElements()) { - // Oh dear - more than one match - // Ambiguous user, can't continue - - } else { - log.debug(LogHelper.getHeader(context, "got DN", resultDN)); - return resultDN; - } + return ctx.getNameInNamespace(); } - } catch (NamingException e) { - // if the lookup fails go ahead and create a new record for them because the authentication - // succeeded - log.warn(LogHelper.getHeader(context, - "ldap_attribute_lookup", "type=failed_search " - + e)); - } - } catch (NamingException | IOException e) { - log.warn(LogHelper.getHeader(context, - "ldap_authentication", "type=failed_auth " + e)); - } finally { - // Close the context when we're done - try { - if (startTLSResponse != null) { - startTLSResponse.close(); - } - if (ctx != null) { - ctx.close(); - } - } catch (NamingException | IOException e) { - // ignore + ); + + if (foundDNs.isEmpty()) { + log.debug(LogHelper.getHeader(context, "getDNOfUser", "no DN found for user " + netid)); + return null; + } else if (foundDNs.size() > 1) { + log.warn(LogHelper.getHeader(context, "getDNOfUser", "multiple DN found for user " + netid)); + return null; } + String resultDN = foundDNs.get(0); + log.debug(LogHelper.getHeader(context, "got DN", resultDN)); + return resultDN; + } catch (Exception e) { + log.warn(LogHelper.getHeader(context, "ldap_authentication", "type=failed_search " + e)); + return null; } - - // No DN match found - return null; } /** * contact the ldap server and attempt to authenticate */ - protected boolean ldapAuthenticate(String netid, String password, - Context context) { - if (!password.equals("")) { - - LdapContext ctx = null; - StartTlsResponse startTLSResponse = null; - - - // Set up environment for creating initial context - @SuppressWarnings("UseOfObsoleteCollectionType") - Hashtable env = new Hashtable<>(); - env.put(javax.naming.Context.INITIAL_CONTEXT_FACTORY, - "com.sun.jndi.ldap.LdapCtxFactory"); - env.put(javax.naming.Context.PROVIDER_URL, ldap_provider_url); - - try { - if (useTLS) { - ctx = new InitialLdapContext(env, null); - // start TLS - startTLSResponse = (StartTlsResponse) ctx - .extendedOperation(new StartTlsRequest()); - - startTLSResponse.negotiate(); - - // perform simple client authentication - ctx.addToEnvironment(javax.naming.Context.SECURITY_AUTHENTICATION, "simple"); - ctx.addToEnvironment(javax.naming.Context.SECURITY_PRINCIPAL, - netid); - ctx.addToEnvironment(javax.naming.Context.SECURITY_CREDENTIALS, - password); - ctx.addToEnvironment(javax.naming.Context.AUTHORITATIVE, "true"); - ctx.addToEnvironment(javax.naming.Context.REFERRAL, "follow"); - // dummy operation to check if authentication has succeeded - @SuppressWarnings("BanJNDI") - Attributes trash = ctx.getAttributes(""); - } else if (!useTLS) { - // Authenticate - env.put(javax.naming.Context.SECURITY_AUTHENTICATION, "Simple"); - env.put(javax.naming.Context.SECURITY_PRINCIPAL, netid); - env.put(javax.naming.Context.SECURITY_CREDENTIALS, password); - env.put(javax.naming.Context.AUTHORITATIVE, "true"); - env.put(javax.naming.Context.REFERRAL, "follow"); - - // Try to bind - ctx = new InitialLdapContext(env, null); - } - } catch (NamingException | IOException e) { - // something went wrong (like wrong password) so return false - log.warn(LogHelper.getHeader(context, - "ldap_authentication", "type=failed_auth " + e)); - return false; - } finally { - // Close the context when we're done - try { - if (startTLSResponse != null) { - startTLSResponse.close(); - } - if (ctx != null) { - ctx.close(); - } - } catch (NamingException | IOException e) { - // ignore - } - } - } else { + protected boolean ldapAuthenticate(String dn, String password, Context context) { + if (StringUtils.isBlank(password)) { return false; } - return true; + try { + boolean authenticated = ldapTemplate.authenticate( + LdapUtils.newLdapName(dn), + new PresentFilter(ldap_id_field).toString(), + password + ); + return authenticated; + + } catch (Exception e) { + log.warn(LogHelper.getHeader(context, "ldap_authentication", "type=failed_auth " + e)); + return false; + } } } + /** * Returns the URL of an external login page which is not applicable for this authn method. * diff --git a/dspace-api/src/main/java/org/dspace/authenticate/OrcidAuthenticationBean.java b/dspace-api/src/main/java/org/dspace/authenticate/OrcidAuthenticationBean.java index 590bbf6cf0ef..be2111fad9bc 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/OrcidAuthenticationBean.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/OrcidAuthenticationBean.java @@ -120,7 +120,7 @@ public String loginPageURL(Context context, HttpServletRequest request, HttpServ @Override public boolean isUsed(Context context, HttpServletRequest request) { - return request.getAttribute(ORCID_AUTH_ATTRIBUTE) != null; + return request != null && request.getAttribute(ORCID_AUTH_ATTRIBUTE) != null; } @Override diff --git a/dspace-api/src/main/java/org/dspace/authenticate/PasswordAuthentication.java b/dspace-api/src/main/java/org/dspace/authenticate/PasswordAuthentication.java index 8e030305c957..035a235422a6 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/PasswordAuthentication.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/PasswordAuthentication.java @@ -188,7 +188,7 @@ public List getSpecialGroups(Context context, HttpServletRequest request) *

Meaning: *
SUCCESS - authenticated OK. *
BAD_CREDENTIALS - user exists, but password doesn't match - *
CERT_REQUIRED - not allowed to login this way without X.509 cert. + *
CERT_REQUIRED - not allowed to login this way without a cert. *
NO_SUCH_USER - no EPerson with matching email address. *
BAD_ARGS - missing username, or user matched but cannot login. * @throws SQLException if database error @@ -213,7 +213,7 @@ public int authenticate(Context context, // cannot login this way return BAD_ARGS; } else if (eperson.getRequireCertificate()) { - // this user can only login with x.509 certificate + // this user can only login with a certificate log.warn(LogHelper.getHeader(context, "authenticate", "rejecting PasswordAuthentication because " + username + " requires " + "certificate.")); diff --git a/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java b/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java index 24d8266012d4..13a5ae6d0dfd 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java @@ -160,7 +160,7 @@ public class ShibAuthentication implements AuthenticationMethod { * SUCCESS - authenticated OK.
* BAD_CREDENTIALS - user exists, but credentials (e.g. passwd) * don't match
- * CERT_REQUIRED - not allowed to login this way without X.509 cert. + * CERT_REQUIRED - not allowed to login this way without a cert. *
* NO_SUCH_USER - user not found using this method.
* BAD_ARGS - user/pw not appropriate for this method @@ -417,8 +417,7 @@ public boolean allowSetPassword(Context context, * Predicate, is this an implicit authentication method. An implicit method * gets credentials from the environment (such as an HTTP request or even * Java system properties) rather than the explicit username and password. - * For example, a method that reads the X.509 certificates in an HTTPS - * request is implicit. + * For example, a method that provides IP-based authentication is implicit. * * @return true if this method uses implicit authentication. */ @@ -871,7 +870,7 @@ protected void updateEPerson(Context context, HttpServletRequest request, EPerso String[] nameParts = MetadataFieldName.parse(field); ePersonService.setMetadataSingleValue(context, eperson, - nameParts[0], nameParts[1], nameParts[2], value, null); + nameParts[0], nameParts[1], nameParts[2], null, value); log.debug("Updated the eperson's '{}' metadata using header: '{}' = '{}'.", field, header, value); } @@ -917,7 +916,7 @@ protected int swordCompatibility(Context context, String username, String passwo " is not allowed to login."); return BAD_ARGS; } else if (eperson.getRequireCertificate()) { - // this user can only login with x.509 certificate + // this user can only login with a certificate log.error( "Shibboleth-based password authentication failed for user " + username + " because the eperson object" + " requires a certificate to authenticate.."); diff --git a/dspace-api/src/main/java/org/dspace/authenticate/X509Authentication.java b/dspace-api/src/main/java/org/dspace/authenticate/X509Authentication.java deleted file mode 100644 index 55843c710760..000000000000 --- a/dspace-api/src/main/java/org/dspace/authenticate/X509Authentication.java +++ /dev/null @@ -1,616 +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.authenticate; - -import java.io.BufferedInputStream; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.security.Principal; -import java.security.PublicKey; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Enumeration; -import java.util.List; -import java.util.StringTokenizer; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.servlet.http.HttpSession; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.logging.log4j.Logger; -import org.dspace.authenticate.factory.AuthenticateServiceFactory; -import org.dspace.authenticate.service.AuthenticationService; -import org.dspace.authorize.AuthorizeException; -import org.dspace.core.Context; -import org.dspace.core.LogHelper; -import org.dspace.eperson.EPerson; -import org.dspace.eperson.Group; -import org.dspace.eperson.factory.EPersonServiceFactory; -import org.dspace.eperson.service.EPersonService; -import org.dspace.eperson.service.GroupService; -import org.dspace.services.ConfigurationService; -import org.dspace.services.factory.DSpaceServicesFactory; - -/** - * Implicit authentication method that gets credentials from the X.509 client - * certificate supplied by the HTTPS client when connecting to this server. The - * email address in that certificate is taken as the authenticated user name - * with no further checking, so be sure your HTTP server (e.g. Tomcat) is - * configured correctly to accept only client certificates it can validate. - *

- * See the AuthenticationMethod interface for more details. - *

- * Configuration: - * - *

- *   x509.keystore.path =
- * 
- * path to Java keystore file
- * 
- *   keystore.password =
- * 
- * password to access the keystore
- * 
- *   ca.cert =
- * 
- * path to certificate file for CA whose client certs to accept.
- * 
- *   autoregister =
- * 
- * "true" if E-Person is created automatically for unknown new users.
- * 
- *   groups =
- * 
- * comma-delimited list of special groups to add user to if authenticated.
- * 
- *   emaildomain =
- * 
- * email address domain (after the 'at' symbol) to match before allowing
- * membership in special groups.
- * 
- * 
- * - * Only one of the "keystore.path" or "ca.cert" - * options is required. If you supply a keystore, then all of the "trusted" - * certificates in the keystore represent CAs whose client certificates will be - * accepted. The ca.cert option only allows a single CA to be - * named. - *

- * You can configure both a keystore and a CA cert, and both will be - * used. - *

- * The autoregister configuration parameter determines what the - * canSelfRegister() method returns. It also allows an EPerson - * record to be created automatically when the presented certificate is - * acceptable but there is no corresponding EPerson. - * - * @author Larry Stone - * @version $Revision$ - */ -public class X509Authentication implements AuthenticationMethod { - - /** - * log4j category - */ - private static Logger log = org.apache.logging.log4j.LogManager.getLogger(X509Authentication.class); - - /** - * public key of CA to check client certs against. - */ - private static PublicKey caPublicKey = null; - - /** - * key store for CA certs if we use that - */ - private static KeyStore caCertKeyStore = null; - - private static String loginPageTitle = null; - - private static String loginPageURL = null; - - protected AuthenticationService authenticationService = AuthenticateServiceFactory.getInstance() - .getAuthenticationService(); - protected EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); - protected GroupService groupService = EPersonServiceFactory.getInstance().getGroupService(); - protected ConfigurationService configurationService = - DSpaceServicesFactory.getInstance().getConfigurationService(); - - private static final String X509_AUTHENTICATED = "x509.authenticated"; - - - /** - * Initialization: Set caPublicKey and/or keystore. This loads the - * information needed to check if a client cert presented is valid and - * acceptable. - */ - static { - ConfigurationService configurationService = - DSpaceServicesFactory.getInstance().getConfigurationService(); - /* - * allow identification of alternative entry points for certificate - * authentication when selected by the user rather than implicitly. - */ - loginPageTitle = configurationService - .getProperty("authentication-x509.chooser.title.key"); - loginPageURL = configurationService - .getProperty("authentication-x509.chooser.uri"); - - String keystorePath = configurationService - .getProperty("authentication-x509.keystore.path"); - String keystorePassword = configurationService - .getProperty("authentication-x509.keystore.password"); - String caCertPath = configurationService - .getProperty("authentication-x509.ca.cert"); - - // First look for keystore full of trusted certs. - if (keystorePath != null) { - FileInputStream fis = null; - if (keystorePassword == null) { - keystorePassword = ""; - } - try { - KeyStore ks = KeyStore.getInstance("JKS"); - fis = new FileInputStream(keystorePath); - ks.load(fis, keystorePassword.toCharArray()); - caCertKeyStore = ks; - } catch (IOException e) { - log - .error("X509Authentication: Failed to load CA keystore, file=" - + keystorePath + ", error=" + e.toString()); - } catch (GeneralSecurityException e) { - log - .error("X509Authentication: Failed to extract CA keystore, file=" - + keystorePath + ", error=" + e.toString()); - } finally { - if (fis != null) { - try { - fis.close(); - } catch (IOException ioe) { - // ignore - } - } - } - } - - // Second, try getting public key out of CA cert, if that's configured. - if (caCertPath != null) { - InputStream is = null; - FileInputStream fis = null; - try { - fis = new FileInputStream(caCertPath); - is = new BufferedInputStream(fis); - X509Certificate cert = (X509Certificate) CertificateFactory - .getInstance("X.509").generateCertificate(is); - if (cert != null) { - caPublicKey = cert.getPublicKey(); - } - } catch (IOException e) { - log.error("X509Authentication: Failed to load CA cert, file=" - + caCertPath + ", error=" + e.toString()); - } catch (CertificateException e) { - log - .error("X509Authentication: Failed to extract CA cert, file=" - + caCertPath + ", error=" + e.toString()); - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException ioe) { - // ignore - } - } - - if (fis != null) { - try { - fis.close(); - } catch (IOException ioe) { - // ignore - } - } - } - } - } - - /** - * Return the email address from certificate, or null if an - * email address cannot be found in the certificate. - *

- * Note that the certificate parsing has only been tested with certificates - * granted by the MIT Certification Authority, and may not work elsewhere. - * - * @param certificate - - * An X509 certificate object - * @return - The email address found in certificate, or null if an email - * address cannot be found in the certificate. - */ - private static String getEmail(X509Certificate certificate) - throws SQLException { - Principal principal = certificate.getSubjectDN(); - - if (principal == null) { - return null; - } - - String dn = principal.getName(); - if (dn == null) { - return null; - } - - StringTokenizer tokenizer = new StringTokenizer(dn, ","); - String token = null; - while (tokenizer.hasMoreTokens()) { - int len = "emailaddress=".length(); - - token = (String) tokenizer.nextToken(); - - if (token.toLowerCase().startsWith("emailaddress=")) { - // Make sure the token actually contains something - if (token.length() <= len) { - return null; - } - - return token.substring(len).toLowerCase(); - } - } - - return null; - } - - /** - * Verify CERTIFICATE against KEY. Return true if and only if CERTIFICATE is - * valid and can be verified against KEY. - * - * @param context The current DSpace context - * @param certificate - - * An X509 certificate object - * @return - True if CERTIFICATE is valid and can be verified against KEY, - * false otherwise. - */ - private static boolean isValid(Context context, X509Certificate certificate) { - if (certificate == null) { - return false; - } - - // This checks that current time is within cert's validity window: - try { - certificate.checkValidity(); - } catch (CertificateException e) { - log.info(LogHelper.getHeader(context, "authentication", - "X.509 Certificate is EXPIRED or PREMATURE: " - + e.toString())); - return false; - } - - // Try CA public key, if available. - if (caPublicKey != null) { - try { - certificate.verify(caPublicKey); - return true; - } catch (GeneralSecurityException e) { - log.info(LogHelper.getHeader(context, "authentication", - "X.509 Certificate FAILED SIGNATURE check: " - + e.toString())); - } - } - - // Try it with keystore, if available. - if (caCertKeyStore != null) { - try { - Enumeration ke = caCertKeyStore.aliases(); - - while (ke.hasMoreElements()) { - String alias = (String) ke.nextElement(); - if (caCertKeyStore.isCertificateEntry(alias)) { - Certificate ca = caCertKeyStore.getCertificate(alias); - try { - certificate.verify(ca.getPublicKey()); - return true; - } catch (CertificateException ce) { - // ignore - } - } - } - log - .info(LogHelper - .getHeader(context, "authentication", - "Keystore method FAILED SIGNATURE check on client cert.")); - } catch (GeneralSecurityException e) { - log.info(LogHelper.getHeader(context, "authentication", - "X.509 Certificate FAILED SIGNATURE check: " - + e.toString())); - } - - } - return false; - } - - /** - * Predicate, can new user automatically create EPerson. Checks - * configuration value. You'll probably want this to be true to take - * advantage of a Web certificate infrastructure with many more users than - * are already known by DSpace. - * - * @throws SQLException if database error - */ - @Override - public boolean canSelfRegister(Context context, HttpServletRequest request, - String username) throws SQLException { - return configurationService - .getBooleanProperty("authentication-x509.autoregister"); - } - - /** - * Nothing extra to initialize. - * - * @throws SQLException if database error - */ - @Override - public void initEPerson(Context context, HttpServletRequest request, - EPerson eperson) throws SQLException { - } - - /** - * We don't use EPerson password so there is no reason to change it. - * - * @throws SQLException if database error - */ - @Override - public boolean allowSetPassword(Context context, - HttpServletRequest request, String username) throws SQLException { - return false; - } - - /** - * Returns true, this is an implicit method. - */ - @Override - public boolean isImplicit() { - return true; - } - - /** - * Returns a list of group names that the user should be added to upon - * successful authentication, configured in dspace.cfg. - * - * @return List of special groups configured for this authenticator - */ - private List getX509Groups() { - List groupNames = new ArrayList(); - - String[] groups = configurationService - .getArrayProperty("authentication-x509.groups"); - - if (ArrayUtils.isNotEmpty(groups)) { - for (String group : groups) { - groupNames.add(group.trim()); - } - } - - return groupNames; - } - - /** - * Checks for configured email domain required to grant special groups - * membership. If no email domain is configured to verify, special group - * membership is simply granted. - * - * @param request - - * The current request object - * @param email - - * The email address from the x509 certificate - */ - private void setSpecialGroupsFlag(HttpServletRequest request, String email) { - String emailDomain = null; - emailDomain = (String) request - .getAttribute("authentication.x509.emaildomain"); - - HttpSession session = request.getSession(true); - - if (null != emailDomain && !"".equals(emailDomain)) { - if (email.substring(email.length() - emailDomain.length()).equals( - emailDomain)) { - session.setAttribute("x509Auth", Boolean.TRUE); - } - } else { - // No configured email domain to verify. Just flag - // as authenticated so special groups are granted. - session.setAttribute("x509Auth", Boolean.TRUE); - } - } - - /** - * Return special groups configured in dspace.cfg for X509 certificate - * authentication. - * - * @param context context - * @param request object potentially containing the cert - * @return An int array of group IDs - * @throws SQLException if database error - */ - @Override - public List getSpecialGroups(Context context, HttpServletRequest request) - throws SQLException { - if (request == null) { - return Collections.EMPTY_LIST; - } - - Boolean authenticated = false; - HttpSession session = request.getSession(false); - authenticated = (Boolean) session.getAttribute("x509Auth"); - authenticated = (null == authenticated) ? false : authenticated; - - if (authenticated) { - List groupNames = getX509Groups(); - List groups = new ArrayList<>(); - - if (groupNames != null) { - for (String groupName : groupNames) { - if (groupName != null) { - Group group = groupService.findByName(context, groupName); - if (group != null) { - groups.add(group); - } else { - log.warn(LogHelper.getHeader(context, - "configuration_error", "unknown_group=" - + groupName)); - } - } - } - } - - return groups; - } - - return Collections.EMPTY_LIST; - } - - /** - * X509 certificate authentication. The client certificate is obtained from - * the ServletRequest object. - *

    - *
  • If the certificate is valid, and corresponds to an existing EPerson, - * and the user is allowed to login, return success.
  • - *
  • If the user is matched but is not allowed to login, it fails.
  • - *
  • If the certificate is valid, but there is no corresponding EPerson, - * the "authentication.x509.autoregister" configuration - * parameter is checked (via canSelfRegister()) - *
      - *
    • If it's true, a new EPerson record is created for the certificate, - * and the result is success.
    • - *
    • If it's false, return that the user was unknown.
    • - *
    - *
  • - *
- * - * @return One of: SUCCESS, BAD_CREDENTIALS, NO_SUCH_USER, BAD_ARGS - * @throws SQLException if database error - */ - @Override - public int authenticate(Context context, String username, String password, - String realm, HttpServletRequest request) throws SQLException { - // Obtain the certificate from the request, if any - X509Certificate[] certs = null; - if (request != null) { - certs = (X509Certificate[]) request - .getAttribute("jakarta.servlet.request.X509Certificate"); - } - - if ((certs == null) || (certs.length == 0)) { - return BAD_ARGS; - } else { - // We have a cert -- check it and get username from it. - try { - if (!isValid(context, certs[0])) { - log - .warn(LogHelper - .getHeader(context, "authenticate", - "type=x509certificate, status=BAD_CREDENTIALS (not valid)")); - return BAD_CREDENTIALS; - } - - // And it's valid - try and get an e-person - String email = getEmail(certs[0]); - EPerson eperson = null; - if (email != null) { - eperson = ePersonService.findByEmail(context, email); - } - if (eperson == null) { - // Cert is valid, but no record. - if (email != null - && canSelfRegister(context, request, null)) { - // Register the new user automatically - log.info(LogHelper.getHeader(context, "autoregister", - "from=x.509, email=" + email)); - - // TEMPORARILY turn off authorisation - context.turnOffAuthorisationSystem(); - eperson = ePersonService.create(context); - eperson.setEmail(email); - eperson.setCanLogIn(true); - authenticationService.initEPerson(context, request, - eperson); - ePersonService.update(context, eperson); - context.dispatchEvents(); - context.restoreAuthSystemState(); - context.setCurrentUser(eperson); - request.setAttribute(X509_AUTHENTICATED, true); - setSpecialGroupsFlag(request, email); - return SUCCESS; - } else { - // No auto-registration for valid certs - log - .warn(LogHelper - .getHeader(context, "authenticate", - "type=cert_but_no_record, cannot auto-register")); - return NO_SUCH_USER; - } - } else if (!eperson.canLogIn()) { // make sure this is a login account - log.warn(LogHelper.getHeader(context, "authenticate", - "type=x509certificate, email=" + email - + ", canLogIn=false, rejecting.")); - return BAD_ARGS; - } else { - log.info(LogHelper.getHeader(context, "login", - "type=x509certificate")); - context.setCurrentUser(eperson); - request.setAttribute(X509_AUTHENTICATED, true); - setSpecialGroupsFlag(request, email); - return SUCCESS; - } - } catch (AuthorizeException ce) { - log.warn(LogHelper.getHeader(context, "authorize_exception", - ""), ce); - } - - return BAD_ARGS; - } - } - - /** - * Returns URL of password-login servlet. - * - * @param context DSpace context, will be modified (EPerson set) upon success. - * @param request The HTTP request that started this operation, or null if not - * applicable. - * @param response The HTTP response from the servlet method. - * @return fully-qualified URL - */ - @Override - public String loginPageURL(Context context, HttpServletRequest request, - HttpServletResponse response) { - return loginPageURL; - } - - @Override - public String getName() { - return "x509"; - } - - @Override - public boolean isUsed(final Context context, final HttpServletRequest request) { - if (request != null && - context.getCurrentUser() != null && - request.getAttribute(X509_AUTHENTICATED) != null) { - return true; - } - return false; - } - - @Override - public boolean canChangePassword(Context context, EPerson ePerson, String currentPassword) { - return false; - } -} diff --git a/dspace-api/src/main/java/org/dspace/authenticate/service/AuthenticationService.java b/dspace-api/src/main/java/org/dspace/authenticate/service/AuthenticationService.java index 45ad8932daec..8409f1e27d05 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/service/AuthenticationService.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/service/AuthenticationService.java @@ -29,11 +29,11 @@ * Configuration
* The stack of authentication methods is defined by one property in the DSpace configuration: *
- *   plugin.sequence.org.dspace.eperson.AuthenticationMethod = a list of method class names
+ *   plugin.sequence.org.dspace.authenticate.AuthenticationMethod = a list of method class names
  *     e.g.
- *   plugin.sequence.org.dspace.eperson.AuthenticationMethod = \
- *       org.dspace.eperson.X509Authentication, \
- *       org.dspace.eperson.PasswordAuthentication
+ *   plugin.sequence.org.dspace.authenticate.AuthenticationMethod = \
+ *       org.dspace.authenticate.IPAuthentication, \
+ *       org.dspace.authenticate.PasswordAuthentication
  * 
*

* The "stack" is always traversed in order, with the methods @@ -64,7 +64,7 @@ public interface AuthenticationService { *

Meaning: *
SUCCESS - authenticated OK. *
BAD_CREDENTIALS - user exists, but credentials (e.g. password) don't match - *
CERT_REQUIRED - not allowed to login this way without X.509 cert. + *
CERT_REQUIRED - not allowed to login this way without a cert. *
NO_SUCH_USER - user not found using this method. *
BAD_ARGS - user/password not appropriate for this method */ @@ -91,7 +91,7 @@ public int authenticate(Context context, *

Meaning: *
SUCCESS - authenticated OK. *
BAD_CREDENTIALS - user exists, but credentials (e.g. password) don't match - *
CERT_REQUIRED - not allowed to login this way without X.509 cert. + *
CERT_REQUIRED - not allowed to login this way without a cert. *
NO_SUCH_USER - user not found using this method. *
BAD_ARGS - user/password not appropriate for this method */ 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 494daa97734a..312a00c146af 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 @@ -10,6 +10,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; @@ -20,6 +21,7 @@ import org.apache.logging.log4j.Logger; import org.dspace.authority.AuthorityValue; import org.dspace.authority.SolrAuthorityInterface; +import org.dspace.external.OrcidConnectionException; import org.dspace.external.OrcidRestConnector; import org.dspace.external.provider.orcid.xml.XMLtoBio; import org.dspace.orcid.model.factory.OrcidFactoryUtils; @@ -142,9 +144,15 @@ public Person getBio(String id) { return null; } initializeAccessToken(); - InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), accessToken); - XMLtoBio converter = new XMLtoBio(); - return converter.convertSinglePerson(bioDocument); + try { + InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), + accessToken); + XMLtoBio converter = new XMLtoBio(); + return converter.convertSinglePerson(bioDocument); + } catch (OrcidConnectionException e) { + log.error("Error retrieving ORCID bio for ID=" + id, e); + return null; + } } @@ -167,29 +175,35 @@ public List queryBio(String text, int start, int rows) { // Check / init access token initializeAccessToken(); - String searchPath = "search?q=" + URLEncoder.encode(text) + "&start=" + start + "&rows=" + rows; + String searchPath = "search?q=" + URLEncoder.encode(text, StandardCharsets.UTF_8) + "&start=" + start + + "&rows=" + rows; log.debug("queryBio searchPath=" + searchPath + " accessToken=" + accessToken); - InputStream bioDocument = orcidRestConnector.get(searchPath, accessToken); - XMLtoBio converter = new XMLtoBio(); - List results = converter.convert(bioDocument); - List bios = new LinkedList<>(); - for (Result result : results) { - OrcidIdentifier orcidIdentifier = result.getOrcidIdentifier(); - if (orcidIdentifier != null) { - log.debug("Found OrcidId=" + orcidIdentifier.toString()); - String orcid = orcidIdentifier.getPath(); - Person bio = getBio(orcid); - if (bio != null) { - bios.add(bio); + try { + InputStream bioDocument = orcidRestConnector.get(searchPath, accessToken); + XMLtoBio converter = new XMLtoBio(); + List results = converter.convert(bioDocument); + List bios = new LinkedList<>(); + for (Result result : results) { + OrcidIdentifier orcidIdentifier = result.getOrcidIdentifier(); + if (orcidIdentifier != null) { + log.debug("Found OrcidId=" + orcidIdentifier); + String orcid = orcidIdentifier.getPath(); + Person bio = getBio(orcid); + if (bio != null) { + bios.add(bio); + } } } + try { + bioDocument.close(); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + return bios; + } catch (OrcidConnectionException e) { + log.error("Error searching ORCID for query=" + text, e); + return Collections.emptyList(); } - try { - bioDocument.close(); - } catch (IOException e) { - log.error(e.getMessage(), e); - } - return bios; } /** 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 f2692cf394fc..bb94198f7961 100644 --- a/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java @@ -16,6 +16,7 @@ import java.util.Arrays; import java.util.Date; import java.util.List; +import java.util.Objects; import java.util.UUID; import org.apache.commons.collections4.CollectionUtils; @@ -48,6 +49,7 @@ import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.eperson.service.GroupService; +import org.dspace.services.ConfigurationService; import org.dspace.workflow.WorkflowItemService; import org.springframework.beans.factory.annotation.Autowired; @@ -84,6 +86,8 @@ public class AuthorizeServiceImpl implements AuthorizeService { protected WorkflowItemService workflowItemService; @Autowired(required = true) private SearchService searchService; + @Autowired(required = true) + private ConfigurationService configurationService; protected AuthorizeServiceImpl() { @@ -508,17 +512,26 @@ public List getPoliciesActionFilter(Context c, DSpaceObject o, return resourcePolicyService.find(c, o, actionID); } + @Override + public void inheritPolicies(Context c, DSpaceObject src, DSpaceObject dest) + throws SQLException, AuthorizeException { + inheritPolicies(c, src, dest, false); + } + @Override public void inheritPolicies(Context c, DSpaceObject src, - DSpaceObject dest) throws SQLException, AuthorizeException { + DSpaceObject dest, boolean includeCustom) throws SQLException, AuthorizeException { // find all policies for the source object List policies = getPolicies(c, src); - //Only inherit non-ADMIN policies (since ADMIN policies are automatically inherited) - //and non-custom policies as these are manually applied when appropriate + // Only inherit non-ADMIN policies (since ADMIN policies are automatically inherited) + // and non-custom policies (usually applied manually?) UNLESS specified otherwise with includCustom + // (for example, item.addBundle() will inherit custom policies to enforce access conditions) List nonAdminPolicies = new ArrayList<>(); for (ResourcePolicy rp : policies) { - if (rp.getAction() != Constants.ADMIN && !StringUtils.equals(rp.getRpType(), ResourcePolicy.TYPE_CUSTOM)) { + if (rp.getAction() != Constants.ADMIN && (!StringUtils.equals(rp.getRpType(), ResourcePolicy.TYPE_CUSTOM) + || (includeCustom && StringUtils.equals(rp.getRpType(), ResourcePolicy.TYPE_CUSTOM) + && isNotAlreadyACustomRPOfThisTypeOnDSO(c, dest)))) { nonAdminPolicies.add(rp); } } @@ -729,15 +742,15 @@ public List getPoliciesActionFilterExceptRpType(Context c, DSpac /** * Checks that the context's current user is a community admin in the site by querying the solr database. + * This query doesn't use authorization inheritance because direct community admin is enough to perform this check. * * @param context context with the current user * @return true if the current user is a community admin in the site * false when this is not the case, or an exception occurred - * @throws java.sql.SQLException passed through. */ @Override - public boolean isCommunityAdmin(Context context) throws SQLException { - return performCheck(context, RESOURCE_TYPE_FIELD + ":" + IndexableCommunity.TYPE); + public boolean isCommunityAdmin(Context context) { + return performCheck(context, RESOURCE_TYPE_FIELD + ":" + IndexableCommunity.TYPE, false); } /** @@ -746,11 +759,10 @@ public boolean isCommunityAdmin(Context context) throws SQLException { * @param context context with the current user * @return true if the current user is a collection admin in the site * false when this is not the case, or an exception occurred - * @throws java.sql.SQLException passed through. */ @Override - public boolean isCollectionAdmin(Context context) throws SQLException { - return performCheck(context, RESOURCE_TYPE_FIELD + ":" + IndexableCollection.TYPE); + public boolean isCollectionAdmin(Context context) { + return performCheck(context, RESOURCE_TYPE_FIELD + ":" + IndexableCollection.TYPE, true); } /** @@ -759,26 +771,26 @@ public boolean isCollectionAdmin(Context context) throws SQLException { * @param context context with the current user * @return true if the current user is an item admin in the site * false when this is not the case, or an exception occurred - * @throws java.sql.SQLException passed through. */ @Override - public boolean isItemAdmin(Context context) throws SQLException { - return performCheck(context, RESOURCE_TYPE_FIELD + ":" + IndexableItem.TYPE); + public boolean isItemAdmin(Context context) { + return performCheck(context, RESOURCE_TYPE_FIELD + ":" + IndexableItem.TYPE, true); } /** * Checks that the context's current user is a community or collection admin in the site. + * This query doesn't use authorization inheritance because direct community/collection admin is enough to + * perform this check. * * @param context context with the current user * @return true if the current user is a community or collection admin in the site * false when this is not the case, or an exception occurred - * @throws java.sql.SQLException passed through. */ @Override - public boolean isComColAdmin(Context context) throws SQLException { + public boolean isComColAdmin(Context context) { return performCheck(context, "(" + RESOURCE_TYPE_FIELD + ":" + IndexableCommunity.TYPE + " OR " + - RESOURCE_TYPE_FIELD + ":" + IndexableCollection.TYPE + ")"); + RESOURCE_TYPE_FIELD + ":" + IndexableCollection.TYPE + ")", false); } /** @@ -793,11 +805,30 @@ public boolean isComColAdmin(Context context) throws SQLException { */ @Override public List findAdminAuthorizedCommunity(Context context, String query, int offset, int limit) - throws SearchServiceException, SQLException { + throws SearchServiceException { + return findAuthorizedCommunityByAction(context, query, Constants.ADMIN, offset, limit); + } + + /** + * Finds communities for which the logged in user has the rights specified by the action parameter. + * + * @param context the context whose user is checked against + * @param query the optional extra query + * @param action the action to check for + * @param offset the offset for pagination + * @param limit the amount of dso's to return + * @return a list of communities for which the logged in user has the rights specified by the action + * @throws SearchServiceException + */ + @Override + public List findAuthorizedCommunityByAction(Context context, String query, int action, int offset, + int limit) + throws SearchServiceException { List communities = new ArrayList<>(); + query = searchService.formatAutoCompleteQuery(query, "dc.title_sort"); query = formatCustomQuery(query); DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + - IndexableCommunity.TYPE, + IndexableCommunity.TYPE, action, true, offset, limit, null, null); for (IndexableObject solrCollections : discoverResult.getIndexableObjects()) { Community community = ((IndexableCommunity) solrCollections).getIndexedObject(); @@ -816,10 +847,26 @@ public List findAdminAuthorizedCommunity(Context context, String quer */ @Override public long countAdminAuthorizedCommunity(Context context, String query) - throws SearchServiceException, SQLException { + throws SearchServiceException { + return countAuthorizedCommunityByAction(context, query, Constants.ADMIN); + } + + /** + * Counts communities for which the current user has the rights specified by the action parameter. + * + * @param context context with the current user + * @param query the query for which to filter the results more + * @param action the action to check for + * @return the matching communities + * @throws SearchServiceException + */ + @Override + public long countAuthorizedCommunityByAction(Context context, String query, int action) + throws SearchServiceException { + query = searchService.formatAutoCompleteQuery(query, "dc.title_sort"); query = formatCustomQuery(query); DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + - IndexableCommunity.TYPE, + IndexableCommunity.TYPE, action, true, null, 0, null, null); return discoverResult.getTotalSearchResults(); } @@ -836,15 +883,34 @@ public long countAdminAuthorizedCommunity(Context context, String query) */ @Override public List findAdminAuthorizedCollection(Context context, String query, int offset, int limit) - throws SearchServiceException, SQLException { + throws SearchServiceException { + return findAuthorizedCollectionByAction(context, query, Constants.ADMIN, offset, limit); + } + + /** + * Finds collections for which the logged in user has the rights specified by the action parameter. + * + * @param context the context whose user is checked against + * @param query the optional extra query + * @param action the action to check for + * @param offset the offset for pagination + * @param limit the amount of dso's to return + * @return a list of collections for which the logged in user has the rights specified by the action + * @throws SearchServiceException + */ + @Override + public List findAuthorizedCollectionByAction(Context context, String query, + int action, int offset, int limit) + throws SearchServiceException { List collections = new ArrayList<>(); if (context.getCurrentUser() == null) { return collections; } + query = searchService.formatAutoCompleteQuery(query, "dc.title_sort"); query = formatCustomQuery(query); DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + - IndexableCollection.TYPE, + IndexableCollection.TYPE, action, true, offset, limit, CollectionService.SOLR_SORT_FIELD, SORT_ORDER.asc); for (IndexableObject solrCollections : discoverResult.getIndexableObjects()) { Collection collection = ((IndexableCollection) solrCollections).getIndexedObject(); @@ -863,31 +929,44 @@ public List findAdminAuthorizedCollection(Context context, String qu */ @Override public long countAdminAuthorizedCollection(Context context, String query) - throws SearchServiceException, SQLException { + throws SearchServiceException { + return countAuthorizedCollectionByAction(context, query, Constants.ADMIN); + } + + /** + * Counts collections for which the current user has the rights specified by the action parameter. + * + * @param context context with the current user + * @param query the query for which to filter the results more + * @param action the action to check for + * @return the matching collections + * @throws SearchServiceException + */ + @Override + public long countAuthorizedCollectionByAction(Context context, String query, int action) + throws SearchServiceException { + query = searchService.formatAutoCompleteQuery(query, "dc.title_sort"); query = formatCustomQuery(query); DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + - IndexableCollection.TYPE, - null, 0, null, null); + IndexableCollection.TYPE, action, + true, null, 0, null, null); return discoverResult.getTotalSearchResults(); } @Override public boolean isAccountManager(Context context) { - try { - return (canCommunityAdminManageAccounts() && isCommunityAdmin(context) - || canCollectionAdminManageAccounts() && isCollectionAdmin(context)); - } catch (SQLException e) { - throw new RuntimeException(e); - } + return (canCommunityAdminManageAccounts() && isCommunityAdmin(context) + || canCollectionAdminManageAccounts() && isCollectionAdmin(context)); } - private boolean performCheck(Context context, String query) throws SQLException { + private boolean performCheck(Context context, String query, boolean inheritAuthorizations) { if (context.getCurrentUser() == null) { return false; } try { - DiscoverResult discoverResult = getDiscoverResult(context, query, null, null, null, null); + DiscoverResult discoverResult = getDiscoverResult(context, query, Constants.ADMIN, inheritAuthorizations, + null, 0, null, null); if (discoverResult.getTotalSearchResults() > 0) { return true; } @@ -899,16 +978,11 @@ private boolean performCheck(Context context, String query) throws SQLException return false; } - private DiscoverResult getDiscoverResult(Context context, String query, Integer offset, Integer limit, - String sortField, SORT_ORDER sortOrder) - throws SearchServiceException, SQLException { - String groupQuery = getGroupToQuery(groupService.allMemberGroups(context, context.getCurrentUser())); + private DiscoverResult getDiscoverResult(Context context, String query, int action, boolean inheritAuthorizations, + Integer offset, Integer limit, String sortField, SORT_ORDER sortOrder) + throws SearchServiceException { DiscoverQuery discoverQuery = new DiscoverQuery(); - if (!this.isAdmin(context)) { - query = query + " AND (" + - "admin:e" + context.getCurrentUser().getID() + groupQuery + ")"; - } discoverQuery.setQuery(query); if (offset != null) { discoverQuery.setStart(offset); @@ -919,28 +993,113 @@ private DiscoverResult getDiscoverResult(Context context, String query, Integer if (sortField != null && sortOrder != null) { discoverQuery.setSortField(sortField, sortOrder); } + discoverQuery.addRequiredAuthorization(action); + discoverQuery.setInheritAuthorizations(inheritAuthorizations); return searchService.search(context, discoverQuery); } - private String getGroupToQuery(List groups) { - StringBuilder groupQuery = new StringBuilder(); + private String formatCustomQuery(String query) { + if (StringUtils.isBlank(query)) { + return ""; + } else { + return query + " AND "; + } + } - if (groups != null) { - for (Group group: groups) { - groupQuery.append(" OR admin:g"); - groupQuery.append(group.getID()); + /** + * Add the default policies, which have not been already added to the given DSpace object + * + * @param context The relevant DSpace Context. + * @param dso The DSpace Object to add policies to + * @param defaultCollectionPolicies list of policies + * @throws SQLException An exception that provides information on a database access error or other errors. + * @throws AuthorizeException Exception indicating the current user of the context does not have permission + * to perform a particular action. + */ + @Override + public void addDefaultPoliciesNotInPlace(Context context, DSpaceObject dso, + List defaultCollectionPolicies) throws SQLException, AuthorizeException { + boolean appendMode = configurationService + .getBooleanProperty("core.authorization.installitem.inheritance-read.append-mode", false); + for (ResourcePolicy defaultPolicy : defaultCollectionPolicies) { + if (!isAnIdenticalPolicyAlreadyInPlace(context, dso, defaultPolicy.getGroup(), Constants.READ, + defaultPolicy.getID()) && + (!appendMode && isNotAlreadyACustomRPOfThisTypeOnDSO(context, dso) || + appendMode && shouldBeAppended(context, dso, defaultPolicy))) { + ResourcePolicy newPolicy = resourcePolicyService.clone(context, defaultPolicy); + newPolicy.setdSpaceObject(dso); + newPolicy.setAction(Constants.READ); + newPolicy.setRpType(ResourcePolicy.TYPE_INHERITED); + resourcePolicyService.update(context, newPolicy); } } + } - return groupQuery.toString(); + /** + * Add a list of custom policies if there are already NO custom policies in place + * + */ + @Override + public void addCustomPoliciesNotInPlace(Context context, DSpaceObject dso, List customPolicies) + throws SQLException, AuthorizeException { + boolean customPoliciesAlreadyInPlace = + findPoliciesByDSOAndType(context, dso, ResourcePolicy.TYPE_CUSTOM).size() > 0; + if (!customPoliciesAlreadyInPlace) { + addPolicies(context, customPolicies, dso); + } } - private String formatCustomQuery(String query) { - if (StringUtils.isBlank(query)) { - return ""; - } else { - return query + " AND "; + /** + * Check whether or not there is already an RP on the given dso, which has actionId={@link Constants.READ} and + * resourceTypeId={@link ResourcePolicy.TYPE_CUSTOM} + * + * @param context DSpace context + * @param dso DSpace object to check for custom read RP + * @return True if there is no RP on the item with custom read RP, otherwise false + * @throws SQLException If something goes wrong retrieving the RP on the DSO + */ + private boolean isNotAlreadyACustomRPOfThisTypeOnDSO(Context context, DSpaceObject dso) throws SQLException { + return isNotAlreadyACustomRPOfThisTypeOnDSO(context, dso, Constants.READ); + } + + private boolean isNotAlreadyACustomRPOfThisTypeOnDSO(Context context, DSpaceObject dso, int action) + throws SQLException { + List rps = resourcePolicyService.find(context, dso, action); + for (ResourcePolicy rp : rps) { + if (rp.getRpType() != null && rp.getRpType().equals(ResourcePolicy.TYPE_CUSTOM)) { + return false; + } } + return true; + } + + /** + * Check if the provided default policy should be appended or not to the final + * item. If an item has at least one custom READ policy any anonymous READ + * policy with empty start/end date should be skipped + * + * @param context DSpace context + * @param dso DSpace object to check for custom read RP + * @param defaultPolicy The policy to check + * @return + * @throws SQLException If something goes wrong retrieving the RP on the DSO + */ + private boolean shouldBeAppended(Context context, DSpaceObject dso, ResourcePolicy defaultPolicy) + throws SQLException { + boolean hasCustomPolicy = resourcePolicyService.find(context, dso, Constants.READ) + .stream() + .filter(rp -> (Objects.nonNull(rp.getRpType()) && + Objects.equals(rp.getRpType(), ResourcePolicy.TYPE_CUSTOM))) + .findFirst() + .isPresent(); + + boolean isAnonymousGroup = Objects.nonNull(defaultPolicy.getGroup()) + && StringUtils.equals(defaultPolicy.getGroup().getName(), Group.ANONYMOUS); + + boolean datesAreNull = Objects.isNull(defaultPolicy.getStartDate()) + && Objects.isNull(defaultPolicy.getEndDate()); + + return !(hasCustomPolicy && isAnonymousGroup && datesAreNull); } } diff --git a/dspace-api/src/main/java/org/dspace/authorize/dao/impl/ResourcePolicyDAOImpl.java b/dspace-api/src/main/java/org/dspace/authorize/dao/impl/ResourcePolicyDAOImpl.java index 3b09f9cf300b..1f9e5ea26677 100644 --- a/dspace-api/src/main/java/org/dspace/authorize/dao/impl/ResourcePolicyDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/authorize/dao/impl/ResourcePolicyDAOImpl.java @@ -182,7 +182,7 @@ public List findByEPersonGroupTypeIdAction(Context context, EPer compareEpersonOrGroups ) ); - return list(context, criteriaQuery, false, ResourcePolicy.class, 1, -1); + return list(context, criteriaQuery, false, ResourcePolicy.class, -1, -1); } @Override diff --git a/dspace-api/src/main/java/org/dspace/authorize/service/AuthorizeService.java b/dspace-api/src/main/java/org/dspace/authorize/service/AuthorizeService.java index e0a94833d76c..95e4ec1ee627 100644 --- a/dspace-api/src/main/java/org/dspace/authorize/service/AuthorizeService.java +++ b/dspace-api/src/main/java/org/dspace/authorize/service/AuthorizeService.java @@ -322,6 +322,19 @@ public void addPolicy(Context c, DSpaceObject o, int actionID, Group g, String t */ public List getPoliciesActionFilterExceptRpType(Context c, DSpaceObject o, int actionID, String rpType) throws SQLException; + /** + * Add policies to an object to match those from a previous object + * + * @param c context + * @param src source of policies + * @param dest destination of inherited policies + * @param includeCustom whether TYPE_CUSTOM policies should be inherited + * @throws SQLException if there's a database problem + * @throws AuthorizeException if the current user is not authorized to add these policies + */ + public void inheritPolicies(Context c, DSpaceObject src, DSpaceObject dest, boolean includeCustom) + throws SQLException, AuthorizeException; + /** * Add policies to an object to match those from a previous object * @@ -546,6 +559,20 @@ void switchPoliciesAction(Context context, DSpaceObject dso, int fromAction, int List findAdminAuthorizedCommunity(Context context, String query, int offset, int limit) throws SearchServiceException, SQLException; + /** + * Finds communities for which the logged in user has the rights specified by the action parameter. + * + * @param context the context whose user is checked against + * @param query the optional extra query + * @param action the action to check for + * @param offset the offset for pagination + * @param limit the amount of dso's to return + * @return a list of communities for which the logged in user has the rights specified by the action + * @throws SearchServiceException + */ + List findAuthorizedCommunityByAction(Context context, String query, int action, int offset, int limit) + throws SearchServiceException, SQLException; + /** * Counts communities for which the current user is admin, AND which match the query. * @@ -558,6 +585,18 @@ List findAdminAuthorizedCommunity(Context context, String query, int long countAdminAuthorizedCommunity(Context context, String query) throws SearchServiceException, SQLException; + /** + * Counts communities for which the current user has the rights specified by the action parameter. + * + * @param context context with the current user + * @param query the query for which to filter the results more + * @param action the action to check for + * @return the matching communities + * @throws SearchServiceException + */ + long countAuthorizedCommunityByAction(Context context, String query, int action) + throws SearchServiceException; + /** * Finds collections for which the current user is admin, AND which match the query. * @@ -567,10 +606,24 @@ long countAdminAuthorizedCommunity(Context context, String query) * @param limit used for pagination of the results * @return the matching collections * @throws SearchServiceException - * @throws SQLException */ List findAdminAuthorizedCollection(Context context, String query, int offset, int limit) - throws SearchServiceException, SQLException; + throws SearchServiceException; + + /** + * Finds collections for which the current user has the rights specified by the action parameter. + * + * @param context context with the current user + * @param query the query for which to filter the results more + * @param action the action to check for + * @param offset used for pagination of the results + * @param limit used for pagination of the results + * @return the matching collections + * @throws SearchServiceException + */ + List findAuthorizedCollectionByAction(Context context, String query, int action, int offset, + int limit) + throws SearchServiceException; /** * Counts collections for which the current user is admin, AND which match the query. @@ -582,7 +635,19 @@ List findAdminAuthorizedCollection(Context context, String query, in * @throws SQLException */ long countAdminAuthorizedCollection(Context context, String query) - throws SearchServiceException, SQLException; + throws SearchServiceException; + + /** + * Counts collections for which the current user has the rights specified by the action parameter. + * + * @param context context with the current user + * @param query the query for which to filter the results more + * @param action the action to check for + * @return the number of matching collections + * @throws SearchServiceException + */ + long countAuthorizedCollectionByAction(Context context, String query, int action) + throws SearchServiceException; /** * Returns true if the current user can manage accounts. @@ -604,4 +669,10 @@ long countAdminAuthorizedCollection(Context context, String query) public void replaceAllPolicies(Context context, DSpaceObject source, DSpaceObject dest) throws SQLException, AuthorizeException; + public void addDefaultPoliciesNotInPlace(Context context, DSpaceObject dso, + List defaultCollectionPolicies) throws SQLException, AuthorizeException; + + public void addCustomPoliciesNotInPlace(Context context, DSpaceObject dso, + List defaultCollectionPolicies) throws SQLException, AuthorizeException; + } diff --git a/dspace-api/src/main/java/org/dspace/browse/BrowseDAO.java b/dspace-api/src/main/java/org/dspace/browse/BrowseDAO.java index 03130e39e78b..26983303a152 100644 --- a/dspace-api/src/main/java/org/dspace/browse/BrowseDAO.java +++ b/dspace-api/src/main/java/org/dspace/browse/BrowseDAO.java @@ -396,4 +396,6 @@ public interface BrowseDAO { public void setStartsWith(String startsWith); public String getStartsWith(); + + public void setDateStartsWith(String dateStartsWith); } 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 be7a34086a46..f7f5f2ff7df7 100644 --- a/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java +++ b/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java @@ -203,12 +203,8 @@ private BrowseInfo browseByItem(BrowserScope bs) // get the table name that we are going to be getting our data from dao.setTable(browseIndex.getTableName()); - if (scope.getBrowseIndex() != null && OrderFormat.TITLE.equals(scope.getBrowseIndex().getDataType())) { - // For browsing by title, apply the same normalization applied to indexed titles - dao.setStartsWith(normalizeJumpToValue(scope.getStartsWith())); - } else { - dao.setStartsWith(StringUtils.lowerCase(scope.getStartsWith())); - } + // Set startsWith or dateStartsWith params on SolrBrowseDAO + addStartsWithParams(bs); // tell the browse query whether we are ascending or descending on the value dao.setAscending(scope.isAscending()); @@ -367,6 +363,30 @@ private BrowseInfo browseByItem(BrowserScope bs) } } + private void addStartsWithParams(BrowserScope bs) throws BrowseException { + if (StringUtils.isNotBlank(scope.getStartsWith())) { + boolean isDateBrowse = bs.getSortOption().getType().equals("date"); + if (!isDateBrowse) { + if (scope.getBrowseIndex() != null + && OrderFormat.TITLE.equals(scope.getBrowseIndex().getDataType())) { + // For browsing by title, apply the same normalization applied to indexed titles + dao.setStartsWith(normalizeJumpToValue(scope.getStartsWith())); + } else { + dao.setStartsWith(StringUtils.lowerCase(scope.getStartsWith())); + } + // clear the old date starts with + dao.setDateStartsWith(null); + } else { + // For "date" sort browses ({@code webui.itemlist.sort-option.*} config): + // sets a date specific filter where the startsWith query is the start date, + // eg `fq=bi_sort_*_sort:+["1940-02" TO + ]` + dao.setDateStartsWith(scope.getStartsWith().trim()); + // clear the old non date starts with + dao.setStartsWith(null); + } + } + } + /** * Browse the archive by single values (such as the name of an author). This * produces a BrowseInfo object that contains Strings as the results of diff --git a/dspace-api/src/main/java/org/dspace/browse/ItemCountDAOSolr.java b/dspace-api/src/main/java/org/dspace/browse/ItemCountDAOSolr.java index e4d0079fe20b..723b49d63aea 100644 --- a/dspace-api/src/main/java/org/dspace/browse/ItemCountDAOSolr.java +++ b/dspace-api/src/main/java/org/dspace/browse/ItemCountDAOSolr.java @@ -7,121 +7,62 @@ */ package org.dspace.browse; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.SolrQuery; +import org.apache.solr.client.solrj.response.QueryResponse; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.DSpaceObject; import org.dspace.core.Context; -import org.dspace.discovery.DiscoverFacetField; -import org.dspace.discovery.DiscoverQuery; -import org.dspace.discovery.DiscoverResult; -import org.dspace.discovery.DiscoverResult.FacetResult; -import org.dspace.discovery.SearchService; -import org.dspace.discovery.SearchServiceException; -import org.dspace.discovery.configuration.DiscoveryConfigurationParameters; +import org.dspace.discovery.SolrSearchCore; import org.dspace.discovery.indexobject.IndexableItem; import org.springframework.beans.factory.annotation.Autowired; /** * Discovery (Solr) driver implementing ItemCountDAO interface to look up item - * count information in communities and collections. Caching operations are - * intentionally not implemented because Solr already is our cache. + * count information in communities and collections. + *

+ * Counts are computed by querying Solr for archived, non-withdrawn, discoverable + * items using {@code location.comm} / {@code location.coll} filters. + * The query returns only {@code numFound} (rows=0), making it very fast. */ public class ItemCountDAOSolr implements ItemCountDAO { - /** - * Log4j logger - */ - private static Logger log = org.apache.logging.log4j.LogManager.getLogger(ItemCountDAOSolr.class); - - /** - * Hold the communities item count obtained from SOLR after the first query. This only works - * well if the ItemCountDAO lifecycle is bound to the request lifecycle as - * it is now. If we switch to a Spring-based instantiation we should mark - * this bean as prototype - **/ - private Map communitiesCount = null; - - /** - * Hold the collection item count obtained from SOLR after the first query - **/ - private Map collectionsCount = null; + private static final Logger log = LogManager.getLogger(ItemCountDAOSolr.class); - /** - * Solr search service - */ @Autowired - protected SearchService searchService; + private SolrSearchCore solrSearchCore; - /** - * Get the count of the items in the given container. - * - * @param context DSpace context - * @param dso DspaceObject - * @return count - */ @Override public int getCount(Context context, DSpaceObject dso) { - loadCount(context); - Integer val = null; + String locationFilter; if (dso instanceof Collection) { - val = collectionsCount.get(dso.getID().toString()); + locationFilter = "location.coll:" + dso.getID().toString(); } else if (dso instanceof Community) { - val = communitiesCount.get(dso.getID().toString()); - } - - if (val != null) { - return val; + locationFilter = "location.comm:" + dso.getID().toString(); } else { return 0; } - } - /** - * make sure that the counts are actually fetched from Solr (if haven't been - * cached in a Map yet) - * - * @param context DSpace Context - */ - private void loadCount(Context context) { - if (communitiesCount != null || collectionsCount != null) { - return; - } - - communitiesCount = new HashMap<>(); - collectionsCount = new HashMap<>(); - - DiscoverQuery query = new DiscoverQuery(); - query.setFacetMinCount(1); - query.addFacetField(new DiscoverFacetField("location.comm", - DiscoveryConfigurationParameters.TYPE_STANDARD, -1, - DiscoveryConfigurationParameters.SORT.COUNT)); - query.addFacetField(new DiscoverFacetField("location.coll", - DiscoveryConfigurationParameters.TYPE_STANDARD, -1, - DiscoveryConfigurationParameters.SORT.COUNT)); - query.addFilterQueries("search.resourcetype:" + IndexableItem.TYPE); // count only items - query.addFilterQueries("NOT(discoverable:false)"); // only discoverable - query.addFilterQueries("withdrawn:false"); // only not withdrawn - query.addFilterQueries("archived:true"); // only archived - query.setMaxResults(0); - - DiscoverResult sResponse; try { - sResponse = searchService.search(context, query); - List commCount = sResponse.getFacetResult("location.comm"); - List collCount = sResponse.getFacetResult("location.coll"); - for (FacetResult c : commCount) { - communitiesCount.put(c.getAsFilterQuery(), (int) c.getCount()); - } - for (FacetResult c : collCount) { - collectionsCount.put(c.getAsFilterQuery(), (int) c.getCount()); + SolrClient solr = solrSearchCore.getSolr(); + if (solr == null) { + return 0; } - } catch (SearchServiceException e) { - log.error("Could not initialize Community/Collection Item Counts from Solr: ", e); + SolrQuery query = new SolrQuery("*:*"); + query.addFilterQuery(locationFilter); + query.addFilterQuery("search.resourcetype:" + IndexableItem.TYPE); + query.addFilterQuery("NOT(discoverable:false)"); + query.addFilterQuery("withdrawn:false"); + query.addFilterQuery("archived:true"); + query.setRows(0); + QueryResponse response = solr.query(query, solrSearchCore.REQUEST_METHOD); + return (int) response.getResults().getNumFound(); + } catch (Exception e) { + log.error("Error counting items in Solr for {}: ", dso.getID(), e); } + return 0; } } 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 a0a7725fa13a..32841e6c4b34 100644 --- a/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java +++ b/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java @@ -11,6 +11,7 @@ import static org.dspace.discovery.SearchUtils.RESOURCE_TYPE_FIELD; import java.io.Serializable; +import java.time.YearMonth; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -99,6 +100,8 @@ public int compare(Object o1, Object o2) { private String startsWith = null; + private String dateStartsWith = null; + /** * field to look for value in */ @@ -221,10 +224,33 @@ private DiscoverResult getSolrResponse() throws BrowseException { } else if (valuePartial) { query.addFilterQueries("{!field f=" + facetField + "_partial}" + value); } + if (StringUtils.isNotBlank(startsWith) && orderField != null) { query.addFilterQueries( "bi_" + orderField + "_sort:" + ClientUtils.escapeQueryChars(startsWith) + "*"); } + if (StringUtils.isNotBlank(dateStartsWith)) { + if (!ascending) { + String raw = dateStartsWith.trim(); + String upperBound; + if (raw.length() == 4) { // YYYY + upperBound = raw + "-12-31"; + } else if (raw.length() == 7) { // YYYY-MM + YearMonth ym = YearMonth.parse(raw); + upperBound = ym.atEndOfMonth().toString(); + } else { // YYYY-MM-DD + upperBound = raw; + } + query.addFilterQueries("bi_" + orderField + "_sort" + ": [* TO \"" + upperBound + "\"]"); + } else { + query.addFilterQueries("bi_" + orderField + "_sort" + ": [\"" + dateStartsWith + "\" TO *]"); + } + } + if (StringUtils.isNotBlank(startsWith) && StringUtils.isNotBlank(dateStartsWith)) { + log.warn(String.format("dateStartsWith %s and startsWith %s both given, only one should " + + "be given since different type of sort filterquery applied depending on which is not blank", + dateStartsWith, startsWith)); + } // filter on item to be sure to don't include any other object // indexed in the Discovery Search core query.addFilterQueries("search.resourcetype:" + IndexableItem.TYPE); @@ -466,6 +492,11 @@ public int getLimit() { return limit; } + @Override + public void setDateStartsWith(String dateStartsWith) { + this.dateStartsWith = dateStartsWith; + } + /* * (non-Javadoc) * diff --git a/dspace-api/src/main/java/org/dspace/checker/CheckerCommand.java b/dspace-api/src/main/java/org/dspace/checker/CheckerCommand.java index c82eb365277d..9ad9f553b445 100644 --- a/dspace-api/src/main/java/org/dspace/checker/CheckerCommand.java +++ b/dspace-api/src/main/java/org/dspace/checker/CheckerCommand.java @@ -131,13 +131,7 @@ public void process() throws SQLException { collector.collect(context, info); } - // UMD Customization - // This change was provided to DSpace in Pull Request 10508 - // This customization markers can be removed once the - // application has been upgraded to a DSpace version containing - // the pull request. context.commit(); - // End UMD Customization bitstream = dispatcher.next(); } } diff --git a/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java b/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java index 50ef4baa98e3..481b055fbb7d 100644 --- a/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java +++ b/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java @@ -75,6 +75,7 @@ public void sendReport(File attachment, int numberOfBitstreams) email.setContent("Checker Report", "report is attached ..."); email.addAttachment(attachment, "checksum_checker_report.txt"); email.addRecipient(configurationService.getProperty("mail.admin")); + log.info("Sending checker report email to " + configurationService.getProperty("mail.admin")); email.send(); } } @@ -109,18 +110,19 @@ public static void main(String[] args) { Options options = new Options(); options.addOption("h", "help", false, "Help"); - options.addOption("d", "Deleted", false, - "Send E-mail report for all bitstreams set as deleted for today"); - options.addOption("m", "Missing", false, - "Send E-mail report for all bitstreams not found in assetstore for today"); - options.addOption("c", "Changed", false, - "Send E-mail report for all bitstreams where checksum has been changed for today"); - options.addOption("a", "All", false, - "Send all E-mail reports"); - options.addOption("u", "Unchecked", false, - "Send the Unchecked bitstream report"); - options.addOption("n", "Not Processed", false, - "Send E-mail report for all bitstreams set to longer be processed for today"); + options.addOption("d", "deleted", false, + "Send email report for all bitstreams set as deleted for today"); + options.addOption("m", "missing", false, + "Send email report for all bitstreams not found in assetstore for today"); + options.addOption("c", "changed", false, + "Send email report for all bitstreams where checksum has been changed for today"); + options.addOption("a", "all", false, + "Send all email reports (used by default)"); + options.addOption("u", "unchecked", false, + "Send the unchecked (i.e. recently added) bitstream email report"); + options.addOption("n", "not-processed", false, + "Send email report for all bitstreams set to no longer be processed for today (includes" + + " bitstreams marked as deleted or not found)"); try { line = parser.parse(options, args); @@ -133,13 +135,15 @@ public static void main(String[] args) { if (line.hasOption('h')) { HelpFormatter myhelp = new HelpFormatter(); - myhelp.printHelp("Checksum Reporter\n", options); - System.out.println("\nSend Deleted bitstream email report: DailyReportEmailer -d"); - System.out.println("\nSend Missing bitstreams email report: DailyReportEmailer -m"); - System.out.println("\nSend Checksum Changed email report: DailyReportEmailer -c"); - System.out.println("\nSend bitstream not to be processed email report: DailyReportEmailer -n"); - System.out.println("\nSend Un-checked bitstream report: DailyReportEmailer -u"); - System.out.println("\nSend All email reports: DailyReportEmailer"); + myhelp.printHelp("checker-emailer\n", options); + System.out.println("\nChecksum Checker Reporter usage examples:\n"); + System.out.println(" - Send all email reports: checker-emailer -a"); + System.out.println(" - Send deleted bitstream email report: checker-emailer -d"); + System.out.println(" - Send missing bitstreams email report: checker-emailer -m"); + System.out.println(" - Send checksum changed email report: checker-emailer -c"); + System.out.println(" - Send bitstream not to be processed email report: checker-emailer -n"); + System.out.println(" - Send unchecked bitstream email report: checker-emailer -u"); + System.out.println("\nDefault (no arguments) is equivalent to 'checker-emailer -a'\n"); System.exit(0); } @@ -191,7 +195,9 @@ public static void main(String[] args) { writer.write("\n--------------------------------- Report Spacer ---------------------------\n\n"); numBitstreams += reporter.getBitstreamNotFoundReport(context, yesterday, tomorrow, writer); writer.write("\n--------------------------------- Report Spacer ---------------------------\n\n"); - numBitstreams += reporter.getNotToBeProcessedReport(context, yesterday, tomorrow, writer); + // not to be processed report includes deleted and not found bitstreams so it is not necessary to + // include the sum in the counter + reporter.getNotToBeProcessedReport(context, yesterday, tomorrow, writer); writer.write("\n--------------------------------- Report Spacer ---------------------------\n\n"); numBitstreams += reporter.getUncheckedBitstreamsReport(context, writer); writer.write("\n--------------------------------- End Report ---------------------------\n\n"); diff --git a/dspace-api/src/main/java/org/dspace/checker/MostRecentChecksumServiceImpl.java b/dspace-api/src/main/java/org/dspace/checker/MostRecentChecksumServiceImpl.java index d267171aa0d9..9ee777a3e15b 100644 --- a/dspace-api/src/main/java/org/dspace/checker/MostRecentChecksumServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/checker/MostRecentChecksumServiceImpl.java @@ -90,61 +90,16 @@ public List findBitstreamResultTypeReport(Context context, D /** * Queries the bitstream table for bitstream IDs that are not yet in the * most_recent_checksum table, and inserts them into the - * most_recent_checksum and checksum_history tables. - * + * most_recent_checksum table. * @param context Context * @throws SQLException if database error */ @Override public void updateMissingBitstreams(Context context) throws SQLException { -// "insert into most_recent_checksum ( " -// + "bitstream_id, to_be_processed, expected_checksum, current_checksum, " -// + "last_process_start_date, last_process_end_date, " -// + "checksum_algorithm, matched_prev_checksum, result ) " -// + "select bitstream.bitstream_id, " -// + "CASE WHEN bitstream.deleted = false THEN true ELSE false END, " -// + "CASE WHEN bitstream.checksum IS NULL THEN '' ELSE bitstream.checksum END, " -// + "CASE WHEN bitstream.checksum IS NULL THEN '' ELSE bitstream.checksum END, " -// + "?, ?, CASE WHEN bitstream.checksum_algorithm IS NULL " -// + "THEN 'MD5' ELSE bitstream.checksum_algorithm END, true, " -// + "CASE WHEN bitstream.deleted = true THEN 'BITSTREAM_MARKED_DELETED' else 'CHECKSUM_MATCH' END " -// + "from bitstream where not exists( " -// + "select 'x' from most_recent_checksum " -// + "where most_recent_checksum.bitstream_id = bitstream.bitstream_id )"; - - List unknownBitstreams = bitstreamService.findBitstreamsWithNoRecentChecksum(context); - for (Bitstream bitstream : unknownBitstreams) { - log.info(bitstream + " " + bitstream.getID().toString() + " " + bitstream.getName()); - - MostRecentChecksum mostRecentChecksum = new MostRecentChecksum(); - mostRecentChecksum.setBitstream(bitstream); - //Only process if our bitstream isn't deleted - mostRecentChecksum.setToBeProcessed(!bitstream.isDeleted()); - if (bitstream.getChecksum() == null) { - mostRecentChecksum.setCurrentChecksum(""); - mostRecentChecksum.setExpectedChecksum(""); - } else { - mostRecentChecksum.setCurrentChecksum(bitstream.getChecksum()); - mostRecentChecksum.setExpectedChecksum(bitstream.getChecksum()); - } - mostRecentChecksum.setProcessStartDate(new Date()); - mostRecentChecksum.setProcessEndDate(new Date()); - if (bitstream.getChecksumAlgorithm() == null) { - mostRecentChecksum.setChecksumAlgorithm("MD5"); - } else { - mostRecentChecksum.setChecksumAlgorithm(bitstream.getChecksumAlgorithm()); - } - mostRecentChecksum.setMatchedPrevChecksum(true); - ChecksumResult checksumResult; - if (bitstream.isDeleted()) { - checksumResult = checksumResultService.findByCode(context, ChecksumResultCode.BITSTREAM_MARKED_DELETED); - } else { - checksumResult = checksumResultService.findByCode(context, ChecksumResultCode.CHECKSUM_MATCH); - } - mostRecentChecksum.setChecksumResult(checksumResult); - mostRecentChecksumDAO.create(context, mostRecentChecksum); - mostRecentChecksumDAO.save(context, mostRecentChecksum); - } + log.info("Retrieving missing bitsreams (bitstream IDs that are not yet in most_recent_checksum table)..."); + int updated = mostRecentChecksumDAO.updateMissingBitstreams(context); + log.info("Updated most_recent_checksum for " + updated + " bitstreams."); + log.info("Missing bitsreams processing done."); } @Override diff --git a/dspace-api/src/main/java/org/dspace/checker/SimpleReporterServiceImpl.java b/dspace-api/src/main/java/org/dspace/checker/SimpleReporterServiceImpl.java index ddefb28e1b57..6c69764fdc79 100644 --- a/dspace-api/src/main/java/org/dspace/checker/SimpleReporterServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/checker/SimpleReporterServiceImpl.java @@ -70,6 +70,7 @@ public int getDeletedBitstreamReport(Context context, Date startDate, Date endDa osw.write("\n"); osw.write(msg("deleted-bitstream-intro")); + osw.write(" "); osw.write(applyDateFormatShort(startDate)); osw.write(" "); osw.write(msg("date-range-to")); @@ -111,7 +112,6 @@ public int getChangedChecksumReport(Context context, Date startDate, Date endDat osw.write("\n"); osw.write(msg("checksum-did-not-match")); osw.write(" "); - osw.write("\n"); osw.write(applyDateFormatShort(startDate)); osw.write(" "); osw.write(msg("date-range-to")); diff --git a/dspace-api/src/main/java/org/dspace/checker/dao/MostRecentChecksumDAO.java b/dspace-api/src/main/java/org/dspace/checker/dao/MostRecentChecksumDAO.java index 56485c9b4b4b..73a81e4d9b4f 100644 --- a/dspace-api/src/main/java/org/dspace/checker/dao/MostRecentChecksumDAO.java +++ b/dspace-api/src/main/java/org/dspace/checker/dao/MostRecentChecksumDAO.java @@ -33,6 +33,8 @@ public List findByNotProcessedInDateRange(Context context, D public List findByResultTypeInDateRange(Context context, Date startDate, Date endDate, ChecksumResultCode resultCode) throws SQLException; + public int updateMissingBitstreams(Context context) throws SQLException; + public void deleteByBitstream(Context context, Bitstream bitstream) throws SQLException; public MostRecentChecksum getOldestRecord(Context context) throws SQLException; diff --git a/dspace-api/src/main/java/org/dspace/checker/dao/impl/MostRecentChecksumDAOImpl.java b/dspace-api/src/main/java/org/dspace/checker/dao/impl/MostRecentChecksumDAOImpl.java index 669621aeeb58..dd07f3e2e9fa 100644 --- a/dspace-api/src/main/java/org/dspace/checker/dao/impl/MostRecentChecksumDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/checker/dao/impl/MostRecentChecksumDAOImpl.java @@ -56,8 +56,8 @@ public List findByNotProcessedInDateRange(Context context, D criteriaQuery.where(criteriaBuilder.and( criteriaBuilder.equal(mostRecentChecksumRoot.get(MostRecentChecksum_.toBeProcessed), false), criteriaBuilder - .lessThanOrEqualTo(mostRecentChecksumRoot.get(MostRecentChecksum_.processStartDate), startDate), - criteriaBuilder.greaterThan(mostRecentChecksumRoot.get(MostRecentChecksum_.processStartDate), endDate) + .lessThanOrEqualTo(mostRecentChecksumRoot.get(MostRecentChecksum_.processStartDate), endDate), + criteriaBuilder.greaterThan(mostRecentChecksumRoot.get(MostRecentChecksum_.processStartDate), startDate) ) ); List orderList = new LinkedList<>(); @@ -66,6 +66,24 @@ public List findByNotProcessedInDateRange(Context context, D return list(context, criteriaQuery, false, MostRecentChecksum.class, -1, -1); } + @Override + public int updateMissingBitstreams(Context context) throws SQLException { + String hql = "INSERT INTO MostRecentChecksum(bitstream, toBeProcessed, expectedChecksum, currentChecksum, " + + "processStartDate, processEndDate, checksumAlgorithm, matchedPrevChecksum, checksumResult) " + + "SELECT b, " + + "CASE WHEN deleted = false THEN true ELSE false END, " + + "CASE WHEN checksum IS NULL THEN '' ELSE checksum END, " + + "CASE WHEN checksum IS NULL THEN '' ELSE checksum END, " + + "current_timestamp(), current_timestamp(), " + + "CASE WHEN checksumAlgorithm IS NULL THEN 'MD5' ELSE checksumAlgorithm END, " + + "CAST(1 AS boolean), " + + "(SELECT cr FROM ChecksumResult AS cr WHERE " + + "(resultCode = 'BITSTREAM_MARKED_DELETED' AND b.deleted = true) " + + "OR (resultCode = 'CHECKSUM_MATCH' AND b.deleted = false)) " + + "FROM Bitstream AS b WHERE NOT EXISTS(SELECT 'x' FROM MostRecentChecksum AS c WHERE c.bitstream = b)"; + Query query = createQuery(context, hql); + return query.executeUpdate(); + } @Override public MostRecentChecksum findByBitstream(Context context, Bitstream bitstream) throws SQLException { diff --git a/dspace-api/src/main/java/org/dspace/checker/service/SimpleReporterService.java b/dspace-api/src/main/java/org/dspace/checker/service/SimpleReporterService.java index 1dc56c20a3de..f3e0b43d8899 100644 --- a/dspace-api/src/main/java/org/dspace/checker/service/SimpleReporterService.java +++ b/dspace-api/src/main/java/org/dspace/checker/service/SimpleReporterService.java @@ -72,7 +72,8 @@ public int getBitstreamNotFoundReport(Context context, Date startDate, Date endD /** * The bitstreams that were set to not be processed report for the specified - * date range. + * date range. This includes bitstreams that are marked as deleted and bitstreams + * that are not found from the assetstore. * * @param context context * @param startDate the start date range. diff --git a/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java index 880a72d0a6c7..4363e0c76bf5 100644 --- a/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java @@ -21,6 +21,8 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; +import org.dspace.app.requestitem.RequestItem; +import org.dspace.app.requestitem.service.RequestItemService; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.dao.BitstreamDAO; @@ -65,6 +67,8 @@ public class BitstreamServiceImpl extends DSpaceObjectServiceImpl imp protected BundleService bundleService; @Autowired(required = true) protected BitstreamStorageService bitstreamStorageService; + @Autowired(required = true) + protected RequestItemService requestItemService; protected BitstreamServiceImpl() { super(); @@ -289,6 +293,13 @@ public void delete(Context context, Bitstream bitstream) throws SQLException, Au //Remove all bundles from the bitstream object, clearing the connection in 2 ways bundles.clear(); + // Remove any RequestItem entities associated with this bitstream ensuring there are no requests referencing + // a deleted bitstream + Iterator requestItems = requestItemService.findByBitstreamId(context, bitstream.getID()); + while (requestItems.hasNext()) { + requestItemService.delete(context, requestItems.next()); + } + // Remove policies only after the bitstream has been updated (otherwise the current user has not WRITE rights) authorizeService.removeAllPolicies(context, bitstream); } @@ -350,6 +361,28 @@ public void expunge(Context context, Bitstream bitstream) throws SQLException, A throw new IllegalStateException("Bitstream " + bitstream.getID().toString() + " must be deleted before it can be removed from the database."); } + + // Defensively remove any remaining bundle2bitstream references. + // Normally delete() already cleans these up, but orphaned rows from + // historical bugs can cause FK constraint violations on hard-delete. + final List bundles = bitstream.getBundles(); + for (Bundle bundle : bundles) { + if (bitstream.equals(bundle.getPrimaryBitstream())) { + bundle.unsetPrimaryBitstreamID(); + } + bundle.removeBitstream(bitstream); + } + bundles.clear(); + + // Remove any orphaned request items referencing this bitstream + Iterator requestItems = requestItemService.findByBitstreamId(context, bitstream.getID()); + while (requestItems.hasNext()) { + requestItemService.delete(context, requestItems.next()); + } + + // Remove any remaining authorization policies + authorizeService.removeAllPolicies(context, bitstream); + bitstreamDAO.delete(context, bitstream); } diff --git a/dspace-api/src/main/java/org/dspace/content/BundleServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/BundleServiceImpl.java index 3ba90c8cc2ae..8234846ed4f3 100644 --- a/dspace-api/src/main/java/org/dspace/content/BundleServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/BundleServiceImpl.java @@ -583,4 +583,8 @@ public Bundle findByLegacyId(Context context, int id) throws SQLException { public int countTotal(Context context) throws SQLException { return bundleDAO.countRows(context); } + + public int countBitstreams(Context context, Bundle bundle) throws SQLException { + return bundleDAO.countBitstreams(context, bundle); + } } 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 f5ef4f4b14a4..2f3fd827bfab 100644 --- a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java @@ -13,18 +13,21 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.MissingResourceException; import java.util.Objects; +import java.util.Queue; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; -import org.apache.solr.client.solrj.util.ClientUtils; import org.dspace.app.util.AuthorizeUtil; import org.dspace.authorize.AuthorizeConfiguration; import org.dspace.authorize.AuthorizeException; @@ -838,6 +841,86 @@ public List findAuthorized(Context context, Community community, int return myResults; } + @Override + public List findAuthorized(Context context, Community community, List actions) + throws SQLException { + + List myCollections = new ArrayList<>(); + EPerson eperson = context.getCurrentUser(); + + //If eperson is Administrator return all colls or if a community is not null only the community's collections + if (authorizeService.isAdmin(context, eperson)) { + if (community != null) { + return community.getCollections(); + } + myCollections = this.findAll(context); + return myCollections; + } + + //Get the collections of the eperson where is is admin of a community + List directGroups = new ArrayList<>(eperson.getGroups()); // direct membership + Queue queue = new LinkedList<>(directGroups); + while (!queue.isEmpty()) { + Group current = queue.poll(); + List parents = current.getParentGroups(); + + for (Group parent : parents) { + if (directGroups.add(parent)) { + queue.add(parent); + } + } + } + + List resourcePolicies = resourcePolicyService + .find(context, eperson, directGroups, Constants.ADMIN, Constants.COMMUNITY); + List uuids = resourcePolicies.stream() + .map(policy -> policy.getdSpaceObject().getID()) + .toList(); + + List communities = uuids.stream() + .map(uuid -> { + try { + return communityService.find(context, uuid); + } catch (SQLException e) { + return null; //ignore that uuid + } + }) + .filter(Objects::nonNull) + .toList(); + + Set allCommunities = new HashSet<>(communities); + Set allCommAdminCollections = communities.stream() + .flatMap(cm -> cm.getCollections().stream()) + .collect(Collectors.toSet()); + Queue queueComm = new LinkedList<>(communities); + + while (!queueComm.isEmpty()) { + Community com = queueComm.poll(); + List childrenComms = com.getSubcommunities(); + for (Community childComm : childrenComms) { + if (allCommunities.add(childComm)) { + queueComm.add(childComm); + allCommAdminCollections.addAll(childComm.getCollections()); + } + } + } + + //Now get the collection when the eperson can deposit or is admin or is in a group with those privileges + myCollections = collectionDAO.findAuthorizedByEPerson(context, eperson, actions); + Set allCollections = new HashSet<>(myCollections); + //Join EPerson Community Admin Collections with Collection Admins + allCollections.addAll(allCommAdminCollections); + + List collsAllowed = new ArrayList<>(allCollections); + + //A community is passed, only the community's collections will be used and existing in eperson Authorizations + if (community != null) { + collsAllowed.retainAll(community.getCollections()); + } + + return collsAllowed; + } + @Override public Collection findByGroup(Context context, Group group) throws SQLException { return collectionDAO.findByGroup(context, group); @@ -949,7 +1032,7 @@ public String getDefaultReadGroupName(Collection collection, String typeOfGroupS @Override public List findCollectionsWithSubmit(String q, Context context, Community community, - int offset, int limit) throws SQLException, SearchServiceException { + int offset, int limit) throws SearchServiceException { List collections = new ArrayList<>(); DiscoverQuery discoverQuery = new DiscoverQuery(); @@ -966,8 +1049,8 @@ public List findCollectionsWithSubmit(String q, Context context, Com } @Override - public int countCollectionsWithSubmit(String q, Context context, Community community) - throws SQLException, SearchServiceException { + public int countCollectionsWithSubmit(Context context, String q, Community community) + throws SearchServiceException { DiscoverQuery discoverQuery = new DiscoverQuery(); discoverQuery.setMaxResults(0); @@ -989,29 +1072,12 @@ public int countCollectionsWithSubmit(String q, Context context, Community commu * terms. The terms are used to make also a prefix query on SOLR * so it can be used to implement an autosuggest feature over the collection name * @return discovery search result objects - * @throws SQLException if something goes wrong * @throws SearchServiceException if search error */ private DiscoverResult retrieveCollectionsWithSubmit(Context context, DiscoverQuery discoverQuery, String entityType, Community community, String q) - throws SQLException, SearchServiceException { - - StringBuilder query = new StringBuilder(); - EPerson currentUser = context.getCurrentUser(); - if (!authorizeService.isAdmin(context)) { - String userId = ""; - if (currentUser != null) { - userId = currentUser.getID().toString(); - } - query.append("submit:(e").append(userId); + throws SearchServiceException { - Set groups = groupService.allMemberGroupsSet(context, currentUser); - for (Group group : groups) { - query.append(" OR g").append(group.getID()); - } - query.append(")"); - discoverQuery.addFilterQueries(query.toString()); - } if (Objects.nonNull(community)) { discoverQuery.addFilterQueries("location.comm:" + community.getID().toString()); } @@ -1019,12 +1085,10 @@ private DiscoverResult retrieveCollectionsWithSubmit(Context context, DiscoverQu discoverQuery.addFilterQueries("search.entitytype:" + entityType); } if (StringUtils.isNotBlank(q)) { - StringBuilder buildQuery = new StringBuilder(); - String escapedQuery = ClientUtils.escapeQueryChars(q); - buildQuery.append("(").append(escapedQuery).append(" OR dc.title_sort:*") - .append(escapedQuery).append("*").append(")"); - discoverQuery.setQuery(buildQuery.toString()); + q = searchService.formatAutoCompleteQuery(q, "dc.title_sort"); + discoverQuery.setQuery(q); } + discoverQuery.addRequiredAuthorization(Constants.ADD); DiscoverResult resp = searchService.search(context, discoverQuery); return resp; } @@ -1064,8 +1128,8 @@ public Collection retrieveCollectionWithSubmitByCommunityAndEntityType(Context c context.turnOffAuthorisationSystem(); List collections; try { - collections = findCollectionsWithSubmit(null, context, community, entityType, 0, 1); - } catch (SQLException | SearchServiceException e) { + collections = findCollectionsWithSubmit(context, null, community, entityType, 0, 1); + } catch (SearchServiceException e) { throw new RuntimeException(e); } context.restoreAuthSystemState(); @@ -1085,8 +1149,8 @@ public Collection retrieveCollectionWithSubmitByCommunityAndEntityType(Context c } @Override - public List findCollectionsWithSubmit(String q, Context context, Community community, String entityType, - int offset, int limit) throws SQLException, SearchServiceException { + public List findCollectionsWithSubmit(Context context, String q, Community community, String entityType, + int offset, int limit) throws SearchServiceException { List collections = new ArrayList<>(); DiscoverQuery discoverQuery = new DiscoverQuery(); discoverQuery.setDSpaceObjectFilter(IndexableCollection.TYPE); @@ -1103,8 +1167,8 @@ public List findCollectionsWithSubmit(String q, Context context, Com } @Override - public int countCollectionsWithSubmit(String q, Context context, Community community, String entityType) - throws SQLException, SearchServiceException { + public int countCollectionsWithSubmit(Context context, String q, Community community, String entityType) + throws SearchServiceException { DiscoverQuery discoverQuery = new DiscoverQuery(); discoverQuery.setMaxResults(0); discoverQuery.setDSpaceObjectFilter(IndexableCollection.TYPE); diff --git a/dspace-api/src/main/java/org/dspace/content/EntityType.java b/dspace-api/src/main/java/org/dspace/content/EntityType.java index 720e0c492ca7..70b054c94d18 100644 --- a/dspace-api/src/main/java/org/dspace/content/EntityType.java +++ b/dspace-api/src/main/java/org/dspace/content/EntityType.java @@ -9,6 +9,7 @@ import java.util.Objects; +import jakarta.persistence.Cacheable; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -19,6 +20,8 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.dspace.core.ReloadableEntity; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; /** * Class representing an EntityType @@ -26,6 +29,8 @@ * This also has a label that will be used to identify what kind of EntityType this object is */ @Entity +@Cacheable +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @Table(name = "entity_type") public class EntityType implements ReloadableEntity { 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 7df892cd56f5..1442c676a0c9 100644 --- a/dspace-api/src/main/java/org/dspace/content/EntityTypeServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/EntityTypeServiceImpl.java @@ -16,6 +16,7 @@ import java.util.Set; import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.response.FacetField; @@ -28,6 +29,7 @@ import org.dspace.content.service.EntityTypeService; import org.dspace.core.Constants; import org.dspace.core.Context; +import org.dspace.discovery.SearchService; import org.dspace.discovery.SolrSearchCore; import org.dspace.discovery.indexobject.IndexableCollection; import org.dspace.eperson.EPerson; @@ -49,6 +51,9 @@ public class EntityTypeServiceImpl implements EntityTypeService { @Autowired protected SolrSearchCore solrSearchCore; + @Autowired + protected SearchService searchService; + @Override public EntityType findByEntityType(Context context, String entityType) throws SQLException { return entityTypeDAO.findByEntityType(context, entityType); @@ -126,26 +131,34 @@ public List getSubmitAuthorizedTypes(Context context) throws SQLException, SolrServerException, IOException { List types = new ArrayList<>(); StringBuilder query = null; - EPerson currentUser = context.getCurrentUser(); if (!authorizeService.isAdmin(context)) { - String userId = ""; + EPerson currentUser = context.getCurrentUser(); + StringBuilder epersonAndGroupClause = new StringBuilder(); if (currentUser != null) { - userId = currentUser.getID().toString(); - query = new StringBuilder(); - query.append("submit:(e").append(userId); + epersonAndGroupClause.append("e").append(currentUser.getID()); } - + //Retrieve all the groups the current user is a member of Set groups = groupService.allMemberGroupsSet(context, currentUser); for (Group group : groups) { - if (query == null) { - query = new StringBuilder(); - query.append("submit:(g"); + if (!epersonAndGroupClause.isEmpty()) { + epersonAndGroupClause.append(" OR g").append(group.getID()); } else { - query.append(" OR g"); + epersonAndGroupClause.append("g").append(group.getID()); } - query.append(group.getID()); } - query.append(")"); + + if (epersonAndGroupClause.isEmpty()) { + // No user or groups, no authorized types + return new ArrayList<>(); + } + query = new StringBuilder(); + query.append("submit:(").append(epersonAndGroupClause).append(")"); + query.append(" OR ").append("admin:(").append(epersonAndGroupClause).append(")"); + String locations = searchService.createLocationQueryForAdministrableDSOs(epersonAndGroupClause.toString()); + if (StringUtils.isNotBlank(locations)) { + query.append(" OR "); + query.append(locations); + } } SolrQuery sQuery = new SolrQuery("*:*"); 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 157b891486f0..f8ea21b3cccb 100644 --- a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java @@ -22,7 +22,6 @@ import java.util.UUID; import java.util.function.Supplier; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -480,7 +479,7 @@ public void addBundle(Context context, Item item, Bundle bundle) throws SQLExcep // now add authorization policies from owning item // hmm, not very "multiple-inclusion" friendly - authorizeService.inheritPolicies(context, item, bundle); + authorizeService.inheritPolicies(context, item, bundle, true); // Add the bundle to in-memory list item.addBundle(bundle); @@ -1046,8 +1045,8 @@ public void adjustBundleBitstreamPolicies(Context context, Item item, Collection // if come from InstallItem: remove all submission/workflow policies authorizeService.removeAllPoliciesByDSOAndType(context, mybundle, ResourcePolicy.TYPE_SUBMISSION); authorizeService.removeAllPoliciesByDSOAndType(context, mybundle, ResourcePolicy.TYPE_WORKFLOW); - addCustomPoliciesNotInPlace(context, mybundle, defaultItemPolicies); - addDefaultPoliciesNotInPlace(context, mybundle, defaultCollectionBundlePolicies); + authorizeService.addCustomPoliciesNotInPlace(context, mybundle, defaultItemPolicies); + authorizeService.addDefaultPoliciesNotInPlace(context, mybundle, defaultCollectionBundlePolicies); for (Bitstream bitstream : mybundle.getBitstreams()) { // If collection has default READ policies, remove the bundle's READ policies. @@ -1093,8 +1092,8 @@ private void removeAllPoliciesAndAddDefault(Context context, Bitstream bitstream throws SQLException, AuthorizeException { authorizeService.removeAllPoliciesByDSOAndType(context, bitstream, ResourcePolicy.TYPE_SUBMISSION); authorizeService.removeAllPoliciesByDSOAndType(context, bitstream, ResourcePolicy.TYPE_WORKFLOW); - addCustomPoliciesNotInPlace(context, bitstream, defaultItemPolicies); - addDefaultPoliciesNotInPlace(context, bitstream, defaultCollectionPolicies); + authorizeService.addCustomPoliciesNotInPlace(context, bitstream, defaultItemPolicies); + authorizeService.addDefaultPoliciesNotInPlace(context, bitstream, defaultCollectionPolicies); } @Override @@ -1132,7 +1131,7 @@ public void adjustItemPolicies(Context context, Item item, Collection collection authorizeService.removeAllPoliciesByDSOAndType(context, item, ResourcePolicy.TYPE_WORKFLOW); // add default policies only if not already in place - addDefaultPoliciesNotInPlace(context, item, defaultCollectionPolicies); + authorizeService.addDefaultPoliciesNotInPlace(context, item, defaultCollectionPolicies); } finally { context.restoreAuthSystemState(); } @@ -1142,11 +1141,6 @@ public void adjustItemPolicies(Context context, Item item, Collection collection public void move(Context context, Item item, Collection from, Collection to) throws SQLException, AuthorizeException, IOException { - // If the two collections are the same, do nothing. - if (from.equals(to)) { - return; - } - // Use the normal move method, and default to not inherit permissions this.move(context, item, from, to, false); } @@ -1161,6 +1155,11 @@ public void move(Context context, Item item, Collection from, Collection to, boo authorizeService.authorizeAction(context, item, Constants.WRITE); } + // If the two collections are the same, do nothing. + if (from.equals(to)) { + return; + } + // Move the Item from one Collection to the other collectionService.addItem(context, to, item); collectionService.removeItem(context, from, item); @@ -1261,43 +1260,41 @@ public boolean canEdit(Context context, Item item) throws SQLException { * * @param context DSpace context * @param discoverQuery + * @param q query string * @return discovery search result objects - * @throws SQLException if something goes wrong * @throws SearchServiceException if search error */ - private DiscoverResult retrieveItemsWithEdit(Context context, DiscoverQuery discoverQuery) - throws SQLException, SearchServiceException { - EPerson currentUser = context.getCurrentUser(); - if (!authorizeService.isAdmin(context)) { - String userId = currentUser != null ? "e" + currentUser.getID().toString() : "e"; - Stream groupIds = groupService.allMemberGroupsSet(context, currentUser).stream() - .map(group -> "g" + group.getID()); - String query = Stream.concat(Stream.of(userId), groupIds) - .collect(Collectors.joining(" OR ", "edit:(", ")")); - discoverQuery.addFilterQueries(query); - } + private DiscoverResult retrieveItemsWithEdit(Context context, DiscoverQuery discoverQuery, String q) + throws SearchServiceException { + if (StringUtils.isNotBlank(q)) { + // Although not all items will have a metadata dc.title, we use it for autocomplete because it is the + // most common. Ideally, we should use a field that all indexed items have + q = searchService.formatAutoCompleteQuery(q, "dc.title_sort"); + discoverQuery.setQuery(q); + } + discoverQuery.addRequiredAuthorization(Constants.WRITE); return searchService.search(context, discoverQuery); } @Override - public List findItemsWithEdit(Context context, int offset, int limit) - throws SQLException, SearchServiceException { + public List findItemsWithEdit(Context context, String q, int offset, int limit) + throws SearchServiceException { DiscoverQuery discoverQuery = new DiscoverQuery(); discoverQuery.setDSpaceObjectFilter(IndexableItem.TYPE); discoverQuery.setStart(offset); discoverQuery.setMaxResults(limit); - DiscoverResult resp = retrieveItemsWithEdit(context, discoverQuery); + DiscoverResult resp = retrieveItemsWithEdit(context, discoverQuery, q); return resp.getIndexableObjects().stream() .map(solrItems -> ((IndexableItem) solrItems).getIndexedObject()) .collect(Collectors.toList()); } @Override - public int countItemsWithEdit(Context context) throws SQLException, SearchServiceException { + public int countItemsWithEdit(Context context, String q) throws SearchServiceException { DiscoverQuery discoverQuery = new DiscoverQuery(); discoverQuery.setMaxResults(0); discoverQuery.setDSpaceObjectFilter(IndexableItem.TYPE); - DiscoverResult resp = retrieveItemsWithEdit(context, discoverQuery); + DiscoverResult resp = retrieveItemsWithEdit(context, discoverQuery, q); return (int) resp.getTotalSearchResults(); } @@ -1322,91 +1319,7 @@ public boolean isInProgressSubmission(Context context, Item item) throws SQLExce */ - /** - * Add the default policies, which have not been already added to the given DSpace object - * - * @param context The relevant DSpace Context. - * @param dso The DSpace Object to add policies to - * @param defaultCollectionPolicies list of policies - * @throws SQLException An exception that provides information on a database access error or other errors. - * @throws AuthorizeException Exception indicating the current user of the context does not have permission - * to perform a particular action. - */ - protected void addDefaultPoliciesNotInPlace(Context context, DSpaceObject dso, - List defaultCollectionPolicies) throws SQLException, AuthorizeException { - boolean appendMode = configurationService - .getBooleanProperty("core.authorization.installitem.inheritance-read.append-mode", false); - for (ResourcePolicy defaultPolicy : defaultCollectionPolicies) { - if (!authorizeService - .isAnIdenticalPolicyAlreadyInPlace(context, dso, defaultPolicy.getGroup(), Constants.READ, - defaultPolicy.getID()) && - (!appendMode && isNotAlreadyACustomRPOfThisTypeOnDSO(context, dso) || - appendMode && shouldBeAppended(context, dso, defaultPolicy))) { - ResourcePolicy newPolicy = resourcePolicyService.clone(context, defaultPolicy); - newPolicy.setdSpaceObject(dso); - newPolicy.setAction(Constants.READ); - newPolicy.setRpType(ResourcePolicy.TYPE_INHERITED); - resourcePolicyService.update(context, newPolicy); - } - } - } - - private void addCustomPoliciesNotInPlace(Context context, DSpaceObject dso, List customPolicies) - throws SQLException, AuthorizeException { - boolean customPoliciesAlreadyInPlace = authorizeService - .findPoliciesByDSOAndType(context, dso, ResourcePolicy.TYPE_CUSTOM).size() > 0; - if (!customPoliciesAlreadyInPlace) { - authorizeService.addPolicies(context, customPolicies, dso); - } - } - - /** - * Check whether or not there is already an RP on the given dso, which has actionId={@link Constants.READ} and - * resourceTypeId={@link ResourcePolicy.TYPE_CUSTOM} - * - * @param context DSpace context - * @param dso DSpace object to check for custom read RP - * @return True if there is no RP on the item with custom read RP, otherwise false - * @throws SQLException If something goes wrong retrieving the RP on the DSO - */ - private boolean isNotAlreadyACustomRPOfThisTypeOnDSO(Context context, DSpaceObject dso) throws SQLException { - List readRPs = resourcePolicyService.find(context, dso, Constants.READ); - for (ResourcePolicy readRP : readRPs) { - if (readRP.getRpType() != null && readRP.getRpType().equals(ResourcePolicy.TYPE_CUSTOM)) { - return false; - } - } - return true; - } - /** - * Check if the provided default policy should be appended or not to the final - * item. If an item has at least one custom READ policy any anonymous READ - * policy with empty start/end date should be skipped - * - * @param context DSpace context - * @param dso DSpace object to check for custom read RP - * @param defaultPolicy The policy to check - * @return - * @throws SQLException If something goes wrong retrieving the RP on the DSO - */ - private boolean shouldBeAppended(Context context, DSpaceObject dso, ResourcePolicy defaultPolicy) - throws SQLException { - boolean hasCustomPolicy = resourcePolicyService.find(context, dso, Constants.READ) - .stream() - .filter(rp -> (Objects.nonNull(rp.getRpType()) && - Objects.equals(rp.getRpType(), ResourcePolicy.TYPE_CUSTOM))) - .findFirst() - .isPresent(); - - boolean isAnonimousGroup = Objects.nonNull(defaultPolicy.getGroup()) - && StringUtils.equals(defaultPolicy.getGroup().getName(), Group.ANONYMOUS); - - boolean datesAreNull = Objects.isNull(defaultPolicy.getStartDate()) - && Objects.isNull(defaultPolicy.getEndDate()); - - return !(hasCustomPolicy && isAnonimousGroup && datesAreNull); - } /** * Returns an iterator of Items possessing the passed metadata field, or only diff --git a/dspace-api/src/main/java/org/dspace/content/RelationshipType.java b/dspace-api/src/main/java/org/dspace/content/RelationshipType.java index ba5f0531e97e..f00b21421e30 100644 --- a/dspace-api/src/main/java/org/dspace/content/RelationshipType.java +++ b/dspace-api/src/main/java/org/dspace/content/RelationshipType.java @@ -7,6 +7,7 @@ */ package org.dspace.content; +import jakarta.persistence.Cacheable; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -20,6 +21,8 @@ import jakarta.persistence.Table; import org.dspace.core.Context; import org.dspace.core.ReloadableEntity; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; @@ -32,6 +35,8 @@ * The cardinality properties describe how many of each relations this relationshipType can support */ @Entity +@Cacheable +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @Table(name = "relationship_type") public class RelationshipType implements ReloadableEntity { diff --git a/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java index 444332df97d2..3aea294b2ba1 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java @@ -34,23 +34,25 @@ * from {@code ${dspace.dir}/config/controlled-vocabularies/*.xml} and turns * them into autocompleting authorities. * - * Configuration: This MUST be configured as a self-named plugin, e.g.: {@code - * plugin.selfnamed.org.dspace.content.authority.ChoiceAuthority = \ + *

Configuration: This MUST be configured as a self-named plugin, e.g.: {@code + * plugin.selfnamed.org.dspace.content.authority.ChoiceAuthority = * org.dspace.content.authority.DSpaceControlledVocabulary * } * - * It AUTOMATICALLY configures a plugin instance for each XML file in the + *

It AUTOMATICALLY configures a plugin instance for each XML file in the * controlled vocabularies directory. The name of the plugin is the basename of * the file; e.g., {@code ${dspace.dir}/config/controlled-vocabularies/nsi.xml} * would generate a plugin called "nsi". * - * Each configured plugin comes with three configuration options: {@code - * vocabulary.plugin._plugin_.hierarchy.store = - * # Store entire hierarchy along with selected value. Default: TRUE - * vocabulary.plugin._plugin_.hierarchy.suggest = - * # Display entire hierarchy in the suggestion list. Default: TRUE - * vocabulary.plugin._plugin_.delimiter = "" - * # Delimiter to use when building hierarchy strings. Default: "::" + *

Each configured plugin comes with three configuration options: + *

    + *
  • {@code vocabulary.plugin._plugin_.hierarchy.store = + * # Store entire hierarchy along with selected value. Default: TRUE}
  • + *
  • {@code vocabulary.plugin._plugin_.hierarchy.suggest = + * # Display entire hierarchy in the suggestion list. Default: TRUE}
  • + *
  • {@code vocabulary.plugin._plugin_.delimiter = "" + * # Delimiter to use when building hierarchy strings. Default: "::"}
  • + *
* } * * @author Michael B. Klein @@ -58,11 +60,23 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements HierarchicalAuthority { - private static Logger log = org.apache.logging.log4j.LogManager.getLogger(DSpaceControlledVocabulary.class); - protected static String xpathTemplate = "//node[contains(translate(@label,'ABCDEFGHIJKLMNOPQRSTUVWXYZ'," + - "'abcdefghijklmnopqrstuvwxyz'),'%s')]"; - protected static String idTemplate = "//node[@id = '%s']"; - protected static String labelTemplate = "//node[@label = '%s']"; + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(); + protected static final String xpathTemplate; + static { + StringBuilder upper = new StringBuilder(); + StringBuilder lower = new StringBuilder(); + for (int cp = 'A'; cp <= Character.MAX_CODE_POINT; cp++) { + if (Character.isLetter(cp) && Character.isUpperCase(cp)) { + int lcp = Character.toLowerCase(cp); + upper.appendCodePoint(cp); + lower.appendCodePoint(lcp); + } + } + xpathTemplate = "//node[contains(translate(@label,'" + upper + "','" + lower + "'),%s)]"; + } + protected static String idTemplate = "//node[@id = %s]"; + protected static String idTemplateQuoted = "//node[@id = '%s']"; + protected static String labelTemplate = "//node[@label = %s]"; protected static String idParentTemplate = "//node[@id = '%s']/parent::isComposedBy/parent::node"; protected static String rootTemplate = "/node"; protected static String pluginNames[] = null; @@ -106,7 +120,7 @@ public boolean accept(File dir, String name) { File.separator + "config" + File.separator + "controlled-vocabularies"; String[] xmlFiles = (new File(vocabulariesPath)).list(new xmlFilter()); - List names = new ArrayList(); + List names = new ArrayList<>(); for (String filename : xmlFiles) { names.add((new File(filename)).getName().replace(".xml", "")); } @@ -162,14 +176,23 @@ protected String buildString(Node node) { public Choices getMatches(String text, int start, int limit, String locale) { init(); log.debug("Getting matches for '" + text + "'"); - String xpathExpression = ""; String[] textHierarchy = text.split(hierarchyDelimiter, -1); + StringBuilder xpathExpressionBuilder = new StringBuilder(); for (int i = 0; i < textHierarchy.length; i++) { - xpathExpression += String.format(xpathTemplate, textHierarchy[i].replaceAll("'", "'").toLowerCase()); + xpathExpressionBuilder.append(String.format(xpathTemplate, "$var" + i)); } + String xpathExpression = xpathExpressionBuilder.toString(); XPath xpath = XPathFactory.newInstance().newXPath(); - int total = 0; - List choices = new ArrayList(); + xpath.setXPathVariableResolver(variableName -> { + String varName = variableName.getLocalPart(); + if (varName.startsWith("var")) { + int index = Integer.parseInt(varName.substring(3)); + return textHierarchy[index].toLowerCase(); + } + throw new IllegalArgumentException("Unexpected variable: " + varName); + }); + int total; + List choices; try { NodeList results = (NodeList) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODESET); total = results.getLength(); @@ -185,14 +208,23 @@ public Choices getMatches(String text, int start, int limit, String locale) { @Override public Choices getBestMatch(String text, String locale) { init(); - log.debug("Getting best matches for '" + text + "'"); - String xpathExpression = ""; + log.debug("Getting best matches for {}'", text); String[] textHierarchy = text.split(hierarchyDelimiter, -1); + StringBuilder xpathExpressionBuilder = new StringBuilder(); for (int i = 0; i < textHierarchy.length; i++) { - xpathExpression += String.format(labelTemplate, textHierarchy[i].replaceAll("'", "'")); + xpathExpressionBuilder.append(String.format(labelTemplate, "$var" + i)); } + String xpathExpression = xpathExpressionBuilder.toString(); XPath xpath = XPathFactory.newInstance().newXPath(); - List choices = new ArrayList(); + xpath.setXPathVariableResolver(variableName -> { + String varName = variableName.getLocalPart(); + if (varName.startsWith("var")) { + int index = Integer.parseInt(varName.substring(3)); + return textHierarchy[index]; + } + throw new IllegalArgumentException("Unexpected variable: " + varName); + }); + List choices; try { NodeList results = (NodeList) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODESET); choices = getChoicesFromNodeList(results, 0, 1); @@ -240,7 +272,7 @@ public Choices getTopChoices(String authorityName, int start, int limit, String @Override public Choices getChoicesByParent(String authorityName, String parentId, int start, int limit, String locale) { init(); - String xpathExpression = String.format(idTemplate, parentId); + String xpathExpression = String.format(idTemplateQuoted, parentId); return getChoicesByXpath(xpathExpression, start, limit); } @@ -264,15 +296,12 @@ public Integer getPreloadLevel() { } private boolean isRootElement(Node node) { - if (node != null && node.getOwnerDocument().getDocumentElement().equals(node)) { - return true; - } - return false; + return node != null && node.getOwnerDocument().getDocumentElement().equals(node); } private Node getNode(String key) throws XPathExpressionException { init(); - String xpathExpression = String.format(idTemplate, key); + String xpathExpression = String.format(idTemplateQuoted, key); Node node = getNodeFromXPath(xpathExpression); return node; } @@ -284,7 +313,7 @@ private Node getNodeFromXPath(String xpathExpression) throws XPathExpressionExce } private List getChoicesFromNodeList(NodeList results, int start, int limit) { - List choices = new ArrayList(); + List choices = new ArrayList<>(); for (int i = 0; i < results.getLength(); i++) { if (i < start) { continue; @@ -303,17 +332,17 @@ private List getChoicesFromNodeList(NodeList results, int start, int lim private Map addOtherInformation(String parentCurr, String noteCurr, List childrenCurr, String authorityCurr) { - Map extras = new HashMap(); + Map extras = new HashMap<>(); if (StringUtils.isNotBlank(parentCurr)) { extras.put("parent", parentCurr); } if (StringUtils.isNotBlank(noteCurr)) { extras.put("note", noteCurr); } - if (childrenCurr.size() > 0) { - extras.put("hasChildren", "true"); - } else { + if (childrenCurr.isEmpty()) { extras.put("hasChildren", "false"); + } else { + extras.put("hasChildren", "true"); } extras.put("id", authorityCurr); return extras; @@ -368,7 +397,7 @@ private String getNote(Node node) { } private List getChildren(Node node) { - List children = new ArrayList(); + List children = new ArrayList<>(); NodeList childNodes = node.getChildNodes(); for (int ci = 0; ci < childNodes.getLength(); ci++) { Node firstChild = childNodes.item(ci); @@ -391,7 +420,7 @@ private List getChildren(Node node) { private boolean isSelectable(Node node) { Node selectableAttr = node.getAttributes().getNamedItem("selectable"); if (null != selectableAttr) { - return Boolean.valueOf(selectableAttr.getNodeValue()); + return Boolean.parseBoolean(selectableAttr.getNodeValue()); } else { // Default is true return true; } @@ -418,7 +447,7 @@ private String getAuthority(Node node) { } private Choices getChoicesByXpath(String xpathExpression, int start, int limit) { - List choices = new ArrayList(); + List choices = new ArrayList<>(); XPath xpath = XPathFactory.newInstance().newXPath(); try { Node parentNode = (Node) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODE); diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/CrosswalkMetadataValidator.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/CrosswalkMetadataValidator.java index b1be458a255e..1bcc1c9ba9fb 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/CrosswalkMetadataValidator.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/CrosswalkMetadataValidator.java @@ -107,9 +107,12 @@ public MetadataField checkMetadata(Context context, String schema, String elemen e.printStackTrace(); } } else if (!fieldChoice.equals("ignore")) { - throw new CrosswalkException( - "The '" + element + "." + qualifier + "' element has not been defined in this DSpace " + - "instance. "); + throw new CrosswalkException(String.format( + "The '%s.%s%s' element has not been defined in this DSpace instance.", + mdSchema.getName(), + element, + qualifier == null ? "" : ("." + qualifier) + )); } } } diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/LicenseStreamDisseminationCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/LicenseStreamDisseminationCrosswalk.java index 46858747870d..b1854bfd85ae 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/LicenseStreamDisseminationCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/LicenseStreamDisseminationCrosswalk.java @@ -8,6 +8,7 @@ package org.dspace.content.crosswalk; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.sql.SQLException; @@ -56,7 +57,12 @@ public void disseminate(Context context, DSpaceObject dso, OutputStream out) Bitstream licenseBs = PackageUtils.findDepositLicense(context, (Item) dso); if (licenseBs != null) { - Utils.copy(bitstreamService.retrieve(context, licenseBs), out); + try (final InputStream bitInputStream = bitstreamService.retrieve(context, licenseBs)) { + Utils.copy(bitInputStream, out); + } catch (Exception e) { + log.warn("Could not retrieve license file for Item with UUID={}. " + + "Leaving it out of generated package. Error{}", dso.getID(), e.getMessage()); + } } } } diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/OREIngestionCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/OREIngestionCrosswalk.java index f756aae22577..9e890a6046fa 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/OREIngestionCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/OREIngestionCrosswalk.java @@ -11,6 +11,8 @@ import java.io.IOException; import java.io.InputStream; import java.net.ConnectException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.sql.SQLException; import java.text.NumberFormat; @@ -18,6 +20,8 @@ import java.util.Date; import java.util.HashSet; import java.util.List; +import java.util.Locale; +import java.util.Objects; import java.util.Set; import org.apache.logging.log4j.Logger; @@ -34,6 +38,8 @@ import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.core.Context; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; import org.jdom2.Attribute; import org.jdom2.Document; import org.jdom2.Element; @@ -76,6 +82,7 @@ public class OREIngestionCrosswalk .getBitstreamFormatService(); protected BundleService bundleService = ContentServiceFactory.getInstance().getBundleService(); protected ItemService itemService = ContentServiceFactory.getInstance().getItemService(); + protected ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); @Override @@ -173,9 +180,13 @@ public void ingest(Context context, DSpaceObject dso, Element root, boolean crea try { // Make sure the url string escapes all the oddball characters String processedURL = encodeForURL(href); - // Generate a requeset for the aggregated resource - ARurl = new URL(processedURL); - in = ARurl.openStream(); + if (validResourceUri(entryId, processedURL)) { + // Generate a request for the aggregated resource + ARurl = new URL(processedURL); + in = ARurl.openStream(); + } else { + throw new FileNotFoundException("Failed to validate " + processedURL); + } } catch (FileNotFoundException fe) { log.error("The provided URI failed to return a resource: " + href); } catch (ConnectException fe) { @@ -219,17 +230,17 @@ public void ingest(Context context, DSpaceObject dso, Element root, boolean crea * @param sourceString source unescaped string */ private String encodeForURL(String sourceString) { - Character lowalpha[] = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', + Character[] lowalpha = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}; - Character upalpha[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', + Character[] upalpha = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}; - Character digit[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; - Character mark[] = {'-', '_', '.', '!', '~', '*', '\'', '(', ')'}; + Character[] digit = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; + Character[] mark = {'-', '_', '.', '!', '~', '*', '\'', '(', ')'}; // reserved - Character reserved[] = {';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '%', '#'}; + Character[] reserved = {';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '%', '#'}; Set URLcharsSet = new HashSet(); URLcharsSet.addAll(Arrays.asList(lowalpha)); @@ -251,4 +262,61 @@ private String encodeForURL(String sourceString) { return processedString.toString(); } + /** + * Validate a resource URI against the host and scheme of the remote OAI endpoint, or a configured + * list of allowed prefixes. + * This still implicitly "trusts" the remote OAI server, but will reject resource URIs with a totally + * different hostname to avoid downloading malicious resources from a compromised endpoint. + * Even if the URL prefix validation is disabled, schemes will still be enforced to http(s) so file:/// and + * other unwanted schemes cannot be used + * @param entryUrl the entryId of the parent ORE resource + * @param resourceUrl the resource URL of the aggregated ORE resource + * @return result of the validation + */ + private boolean validResourceUri(String entryUrl, String resourceUrl) { + try { + Set allowedSchemes = Set.of("http", "https"); + URI entryUri = new URI(entryUrl).normalize(); + URI resourceUri = new URI(resourceUrl).normalize(); + String scheme = resourceUri.getScheme(); + + if (scheme == null || + !allowedSchemes.contains(scheme.toLowerCase(Locale.ROOT))) { + log.warn("Illegal scheme requested for ORE resource: {}", resourceUri); + return false; + } + + if (configurationService.getBooleanProperty("oai.harvester.ore.file.validateUrlPrefix", false)) { + for (String allowedPrefix : configurationService + .getArrayProperty("oai.harvester.ore.file.allowedUrlPrefix")) { + URI allowedUri = new URI(allowedPrefix).normalize(); + // Return true on the first allowed prefix match + if (Objects.equals(resourceUri.getScheme(), allowedUri.getScheme()) + && Objects.equals(resourceUri.getHost().toLowerCase(Locale.ROOT), + allowedUri.getHost().toLowerCase(Locale.ROOT))) { + return true; + } + } + + // If no allowed prefixes were matched, we require scheme + host to match the remote OAI server + if (!Objects.equals(entryUri.getScheme(), resourceUri.getScheme())) { + log.warn("Illegal scheme requested for ORE resource: {}", resourceUri); + return false; + } + if (!Objects.equals( + entryUri.getHost().toLowerCase(Locale.ROOT), + resourceUri.getHost().toLowerCase(Locale.ROOT))) { + log.warn("Illegal host requested for ORE resource: {}", resourceUri); + return false; + } + } + + return true; + + } catch (URISyntaxException e) { + log.warn("Could not validate ORE resource URI: {}", resourceUrl); + return false; + } + } + } diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java index 39b6c8f29c80..6b6c0fd7c5a4 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java @@ -20,9 +20,7 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.content.Bitstream; import org.dspace.content.BitstreamFormat; -import org.dspace.content.Bundle; import org.dspace.content.DSpaceObject; -import org.dspace.content.Item; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.BitstreamFormatService; import org.dspace.content.service.BitstreamService; @@ -224,29 +222,17 @@ public Element disseminateElement(Context context, DSpaceObject dso) // c. made-up name based on sequence ID and extension. String sid = String.valueOf(bitstream.getSequenceID()); String baseUrl = configurationService.getProperty("dspace.ui.url"); - String handle = null; - // get handle of parent Item of this bitstream, if there is one: - List bn = bitstream.getBundles(); - if (bn.size() > 0) { - List bi = bn.get(0).getItems(); - if (bi.size() > 0) { - handle = bi.get(0).getHandle(); - } - } // get or make up name for bitstream: String bsName = bitstream.getName(); if (bsName == null) { List ext = bitstream.getFormat(context).getExtensions(); bsName = "bitstream_" + sid + (ext.size() > 0 ? ext.get(0) : ""); } - if (handle != null && baseUrl != null) { + if (baseUrl != null) { oiv.setText(baseUrl - + "/bitstream/" - + URLEncoder.encode(handle, "UTF-8") - + "/" - + sid - + "/" - + URLEncoder.encode(bsName, "UTF-8")); + + "/bitstreams/" + + bitstream.getID() + + "/download"); } else { oiv.setText(URLEncoder.encode(bsName, "UTF-8")); } diff --git a/dspace-api/src/main/java/org/dspace/content/dao/BundleDAO.java b/dspace-api/src/main/java/org/dspace/content/dao/BundleDAO.java index da7435d46643..99abd84eabc9 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/BundleDAO.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/BundleDAO.java @@ -22,4 +22,6 @@ */ public interface BundleDAO extends DSpaceObjectLegacySupportDAO { int countRows(Context context) throws SQLException; + + int countBitstreams(Context context, Bundle bundle) throws SQLException; } diff --git a/dspace-api/src/main/java/org/dspace/content/dao/CollectionDAO.java b/dspace-api/src/main/java/org/dspace/content/dao/CollectionDAO.java index 6bb65bbb46d8..13bcf5f52c02 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/CollectionDAO.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/CollectionDAO.java @@ -48,6 +48,18 @@ public List findAll(Context context, MetadataField order, Integer li List findAuthorizedByGroup(Context context, EPerson ePerson, List actions) throws SQLException; + /** + * Get all authorized collections of the current EPerson + * + * @param context DSpace context object + * @param ePerson the current EPerson + * @param actions list of actionsID ADD, READ, etc. + * @return the collections the eperson is defined + * @throws SQLException if database error + */ + List findAuthorizedByEPerson(Context context, EPerson ePerson, List actions) + throws SQLException; + List findCollectionsWithSubscribers(Context context) throws SQLException; int countRows(Context context) throws SQLException; diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/BitstreamDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/BitstreamDAOImpl.java index 25f102f6def4..66a775e39d80 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/BitstreamDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/BitstreamDAOImpl.java @@ -178,7 +178,7 @@ public int countDeleted(Context context) throws SQLException { @Override public int countWithNoPolicy(Context context) throws SQLException { Query query = createQuery(context, - "SELECT count(bit.id) from Bitstream bit where bit.deleted<>true and bit.id not in" + + "SELECT count(bit.id) from Bitstream bit where bit.deleted<>true and bit not in" + " (select res.dSpaceObject from ResourcePolicy res where res.resourceTypeId = " + ":typeId )"); query.setParameter("typeId", Constants.BITSTREAM); diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/BundleDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/BundleDAOImpl.java index 991636108495..ded13687aa7d 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/BundleDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/BundleDAOImpl.java @@ -9,6 +9,7 @@ import java.sql.SQLException; +import jakarta.persistence.Query; import org.dspace.content.Bundle; import org.dspace.content.dao.BundleDAO; import org.dspace.core.AbstractHibernateDSODAO; @@ -31,4 +32,13 @@ protected BundleDAOImpl() { public int countRows(Context context) throws SQLException { return count(createQuery(context, "SELECT count(*) from Bundle")); } + + @Override + public int countBitstreams(Context context, Bundle bundle) throws SQLException { + Query query = createQuery( + context, "SELECT count(bi.id) from Bundle bu join bu.bitstreams bi where bu.id = :bundleID" + ); + query.setParameter("bundleID", bundle.getID()); + return count(query); + } } diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java index 841da319f0b2..e47b1ed4a02b 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java @@ -10,8 +10,13 @@ import java.sql.SQLException; import java.util.AbstractMap; import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.UUID; import jakarta.persistence.Query; import jakarta.persistence.criteria.CriteriaBuilder; @@ -19,6 +24,7 @@ import jakarta.persistence.criteria.Join; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; +import org.apache.logging.log4j.Logger; import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.ResourcePolicy_; import org.dspace.content.Collection; @@ -40,6 +46,11 @@ * @author kevinvandevelde at atmire.com */ public class CollectionDAOImpl extends AbstractHibernateDSODAO implements CollectionDAO { + /** + * log4j logger + */ + private static Logger log = org.apache.logging.log4j.LogManager.getLogger(CollectionDAOImpl.class); + protected CollectionDAOImpl() { super(); } @@ -157,9 +168,103 @@ public List findAuthorizedByGroup(Context context, EPerson ePerson, } + /** + * Get all authorized collections of the current EPerson + * + * @param context DSpace context object + * @param ePerson the current EPerson + * @param actions list of actionsID ADD, READ, etc. + * @return the collections the eperson is defined + * @throws SQLException if database error + */ + @Override + public List findAuthorizedByEPerson(Context context, EPerson ePerson, List actions) + throws SQLException { + + //NOTE steps 1) and 2) removes the need of WITH RECURSIVE and a NativeQuery + + // 1) Get all groups a eperson belongs + /*ArrayList<>(ePerson.getGroups()) - This ensures you have a concrete copy and can modify it safely. + instead if List directGroups = ePerson.getGroups(); + Also - Can be done using this query: + List directGroups = createQuery(context, """ + SELECT g + FROM Group g + JOIN g.epeople e + WHERE e.id = :epersonId + """) + .setParameter("epersonId", ePerson.getID()) + .getResultList(); + */ + List directGroups = new ArrayList<>(ePerson.getGroups()); // direct membership + + // 2) Expand hierarquy of groups in memory (recursively) + Set allGroups = new HashSet<>(directGroups); + Queue queue = new LinkedList<>(directGroups); + + /* + * Using the query avoids the change of the getParentGroups visibility in Group + * The List parents = current.getParentGroups() could be achieved using: + * List parents = createQuery(context,""" + SELECT g + FROM Group g + JOIN g.groups child + WHERE child = :child + """) + */ + // //current.getMemberGroups()- Making public getParentGroups in Group Class (why it isn't already public?) + while (!queue.isEmpty()) { + Group current = queue.poll(); + List parents = current.getParentGroups(); + + for (Group parent : parents) { + if (allGroups.add(parent)) { + queue.add(parent); + } + } + } + + CriteriaBuilder cb = getCriteriaBuilder(context); + CriteriaQuery cq = getCriteriaQuery(cb, Collection.class); + Root collectionRoot = cq.from(Collection.class); + + // Join to ResourcePolicy using metamodel + Join rpJoin = collectionRoot.join("resourcePolicies"); + // Use metamodel for typesafe access + cq.select(collectionRoot).distinct(true); + + List predicates = new ArrayList<>(actions.size()); + // WHERE rp.resourceTypeId = :resourceType + predicates.add(cb.equal(rpJoin.get(ResourcePolicy_.resourceTypeId), Constants.COLLECTION)); + // AND (:hasActions = false OR rp.actionId IN :actionIds) + if (actions != null && !actions.isEmpty()) { + predicates.add(rpJoin.get(ResourcePolicy_.actionId).in(actions)); + } + + // AND (rp.eperson.id = :epersonId OR (:hasGroups = true AND rp.epersonGroup.id IN :groupIds)) + Predicate epersonPredicate = cb.equal( + rpJoin.get(ResourcePolicy_.eperson), ePerson + ); + // Using only groups instead of groupsIDs + Predicate groupPredicate = cb.disjunction(); // false by default + if (allGroups != null && !allGroups.isEmpty()) { + groupPredicate = rpJoin.get(ResourcePolicy_.epersonGroup).in(allGroups); + } + + // Combine access condition + Predicate accessPredicate = cb.or(epersonPredicate, groupPredicate); + predicates.add(accessPredicate); + + // Apply WHERE clause + cq.where(cb.and(predicates.toArray(new Predicate[0]))); + + // Execute + return list(context, cq, true, Collection.class, -1, -1); + } + @Override public List findCollectionsWithSubscribers(Context context) throws SQLException { - return list(createQuery(context, "SELECT DISTINCT c FROM Collection c JOIN Subscription s ON c.id = " + + return list(createQuery(context, "SELECT DISTINCT c FROM Collection c JOIN Subscription s ON c = " + "s.dSpaceObject")); } @@ -172,15 +277,26 @@ public int countRows(Context context) throws SQLException { @SuppressWarnings("unchecked") public List> getCollectionsWithBitstreamSizesTotal(Context context) throws SQLException { - String q = "select col as collection, sum(bit.sizeBytes) as totalBytes from Item i join i.collections col " + - "join i.bundles bun join bun.bitstreams bit group by col"; + String q = "select col.id, sum(bit.sizeBytes) as totalBytes from Item i join i.collections col " + + "join i.bundles bun join bun.bitstreams bit group by col.id"; Query query = createQuery(context, q); + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + List list = query.getResultList(); List> returnList = new ArrayList<>(list.size()); for (Object[] o : list) { - returnList.add(new AbstractMap.SimpleEntry<>((Collection) o[0], (Long) o[1])); + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Collection.class); + Root collectionRoot = criteriaQuery.from(Collection.class); + criteriaQuery.select(collectionRoot).where(criteriaBuilder.equal(collectionRoot.get("id"), (UUID) o[0])); + Query collectionQuery = createQuery(context, criteriaQuery); + Collection collection = (Collection) collectionQuery.getSingleResult(); + if (collection != null) { + returnList.add(new AbstractMap.SimpleEntry<>(collection, (Long) o[1])); + } else { + log.warn("Unable to find Collection with UUID: {}", o[0]); + } } return returnList; } -} \ No newline at end of file +} diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/EntityTypeDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/EntityTypeDAOImpl.java index 32af7ed35c31..8bbbbf4996f5 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/EntityTypeDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/EntityTypeDAOImpl.java @@ -53,7 +53,7 @@ public List getEntityTypesByNames(Context context, List name orderList.add(criteriaBuilder.desc(entityTypeRoot.get(EntityType_.label))); criteriaQuery.select(entityTypeRoot).orderBy(orderList); criteriaQuery.where(entityTypeRoot.get(EntityType_.LABEL).in(names)); - return list(context, criteriaQuery, false, EntityType.class, limit, offset); + return list(context, criteriaQuery, true, EntityType.class, limit, offset); } @Override diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipTypeDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipTypeDAOImpl.java index 7b0e33fd41d9..0deb5afb0a5e 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipTypeDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipTypeDAOImpl.java @@ -46,7 +46,7 @@ public RelationshipType findbyTypesAndTypeName(Context context, EntityType leftT criteriaBuilder.equal(relationshipTypeRoot.get(RelationshipType_.rightType), rightType), criteriaBuilder.equal(relationshipTypeRoot.get(RelationshipType_.leftwardType), leftwardType), criteriaBuilder.equal(relationshipTypeRoot.get(RelationshipType_.rightwardType), rightwardType))); - return uniqueResult(context, criteriaQuery, false, RelationshipType.class); + return uniqueResult(context, criteriaQuery, true, RelationshipType.class); } @Override @@ -96,7 +96,7 @@ public List findByEntityType(Context context, EntityType entit List orderList = new LinkedList<>(); orderList.add(criteriaBuilder.asc(relationshipTypeRoot.get(RelationshipType_.ID))); criteriaQuery.orderBy(orderList); - return list(context, criteriaQuery, false, RelationshipType.class, limit, offset); + return list(context, criteriaQuery, true, RelationshipType.class, limit, offset); } @Override @@ -122,7 +122,7 @@ public List findByEntityType(Context context, EntityType entit criteriaBuilder.equal(relationshipTypeRoot.get(RelationshipType_.rightType), entityType) ); } - return list(context, criteriaQuery, false, RelationshipType.class, limit, offset); + return list(context, criteriaQuery, true, RelationshipType.class, limit, offset); } @Override diff --git a/dspace-api/src/main/java/org/dspace/content/logic/condition/ReadableByGroupCondition.java b/dspace-api/src/main/java/org/dspace/content/logic/condition/ReadableByGroupCondition.java index 20138beb47ef..e7b0bb7e046c 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/condition/ReadableByGroupCondition.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/condition/ReadableByGroupCondition.java @@ -49,7 +49,7 @@ public boolean getResult(Context context, Item item) throws LogicalStatementExce List policies = authorizeService .getPoliciesActionFilter(context, item, Constants.getActionID(action)); for (ResourcePolicy policy : policies) { - if (policy.getGroup().getName().equals(group)) { + if (policy.getGroup() != null && policy.getGroup().getName().equals(group)) { return true; } } diff --git a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSDisseminator.java b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSDisseminator.java index fd50ec8023e2..2161a222e1f2 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSDisseminator.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSDisseminator.java @@ -456,17 +456,37 @@ protected void addBitstreamsToZip(Context context, DSpaceObject dso, // contents are unchanged ze.setTime(DEFAULT_MODIFIED_DATE); } - ze.setSize(auth ? bitstream.getSizeBytes() : 0); - zip.putNextEntry(ze); + + long bitstreamSize = 0; + // If user is authorized to read this bitstream, attempt to retrieve it. if (auth) { - InputStream input = bitstreamService.retrieve(context, bitstream); - Utils.copy(input, zip); - input.close(); + try (final InputStream bitstreamInput = bitstreamService.retrieve(context, bitstream)) { + // Save bitstream size into Zip entry & put entry in Zip file. + bitstreamSize = bitstream.getSizeBytes(); + ze.setSize(bitstreamSize); + zip.putNextEntry(ze); + + // Copy bitstream contents to Zip file + Utils.copy(bitstreamInput, zip); + } catch (Exception e) { + log.warn("Adding zero-length file for Bitstream, uuid={}." + + " Bitstream is unable to be retrieved from assetstore." + + " Error={}", bitstream.getID(), e.getMessage()); + } } else { - log.warn("Adding zero-length file for Bitstream, uuid=" - + String.valueOf(bitstream.getID()) - + ", not authorized for READ."); + log.warn("Adding zero-length file for Bitstream, uuid={}" + + ", not authorized for READ.", bitstream.getID()); + } + + // If bitstreamSize is still zero, that means either we didn't have READ privileges + // or the bitstream could not be retrieved from storage. Either way, write a zero-length + // file into our Zip entry in place of the bitstream. + if (bitstreamSize == 0) { + ze.setSize(0); + zip.putNextEntry(ze); } + + // Close our zip entry zip.closeEntry(); } else if (unauth != null && unauth.equalsIgnoreCase("skip")) { log.warn("Skipping Bitstream, uuid=" + String @@ -629,6 +649,13 @@ protected MdSec makeMdSec(Context context, DSpaceObject dso, Class mdSecClass, ByteArrayOutputStream disseminateOutput = new ByteArrayOutputStream(); sxwalk.disseminate(context, dso, disseminateOutput); disseminateOutput.close(); + + // If our disseminated output has zero size, exit immediately (i.e. return a null mdSec). + // Likely, the outputstream failed to be created, so we cannot include it in this package. + if (disseminateOutput.size() == 0) { + return null; + } + // Convert output to an inputstream, so we can write to manifest or Zip file ByteArrayInputStream crosswalkedStream = new ByteArrayInputStream( disseminateOutput.toByteArray()); 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 77236be9d525..122f13992087 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 @@ -19,12 +19,16 @@ import java.util.zip.ZipFile; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.io.input.NullInputStream; 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.authorize.ResourcePolicy; +import org.dspace.authorize.factory.AuthorizeServiceFactory; +import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.Bitstream; import org.dspace.content.BitstreamFormat; import org.dspace.content.Bundle; @@ -127,6 +131,10 @@ public abstract class AbstractMETSIngester extends AbstractPackageIngester { protected final ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + + protected final AuthorizeService authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService(); + + /** *

* An instance of ZipMdrefManager holds the state needed to retrieve the @@ -498,8 +506,11 @@ protected DSpaceObject ingestObject(Context context, DSpaceObject parent, // Finish creating the item. This actually assigns the handle, // and will either install item immediately or start a workflow, based on params PackageUtils.finishCreateItem(context, wsi, handle, params); + } else { + // We should have a workspace item during ingest, so this code is only here for safety. + // Update the object to make sure all changes are committed + PackageUtils.updateDSpaceObject(context, dso); } - } else if (type == Constants.COLLECTION || type == Constants.COMMUNITY) { // Add logo if one is referenced from manifest addContainerLogo(context, dso, manifest, pkgFile, params); @@ -513,6 +524,9 @@ protected DSpaceObject ingestObject(Context context, DSpaceObject parent, // (this allows subclasses to do some final validation / changes as // necessary) finishObject(context, dso, params); + + // Update the object to make sure all changes are committed + PackageUtils.updateDSpaceObject(context, dso); } else if (type == Constants.SITE) { // Do nothing by default -- Crosswalks will handle anything necessary to ingest at Site-level @@ -520,18 +534,15 @@ protected DSpaceObject ingestObject(Context context, DSpaceObject parent, // (this allows subclasses to do some final validation / changes as // necessary) finishObject(context, dso, params); + + // Update the object to make sure all changes are committed + PackageUtils.updateDSpaceObject(context, dso); } else { throw new PackageValidationException( "Unknown DSpace Object type in package, type=" + String.valueOf(type)); } - // -- Step 6 -- - // Finish things up! - - // Update the object to make sure all changes are committed - PackageUtils.updateDSpaceObject(context, dso); - return dso; } @@ -738,6 +749,14 @@ protected void addBitstreams(Context context, Item item, // externally, if it is an externally referenced file) InputStream fileStream = getFileInputStream(pkgFile, params, path); + // Before proceeding we must ensure we have a non-empty input stream + // NOTE: If getFileInputStream encounters a zero-sized file, then it returns NullInputStream + if (fileStream == null || fileStream instanceof NullInputStream) { + log.warn("Empty InputStream encountered for Bitstream with ID={} in zip file={}. " + + "Skipping adding this empty bitstream to Item={}", mfileID, pkgFile, item.getID()); + continue; + } + // retrieve bundle name from manifest String bundleName = METSManifest.getBundleName(mfile); @@ -760,11 +779,24 @@ protected void addBitstreams(Context context, Item item, bitstream.setSequenceID(Integer.parseInt(seqID)); } + // Get TYPE_SUBMISSION policies before removing them in the `manifest.crosswalkBitstream` method. + List bitstreamPolicies = + authorizeService.findPoliciesByDSOAndType(context, bitstream, ResourcePolicy.TYPE_SUBMISSION); + // crosswalk this bitstream's administrative metadata located in // METS manifest (or referenced externally) manifest.crosswalkBitstream(context, params, bitstream, mfileID, mdRefCallback); + // Only add the saved TYPE_SUBMISSION policies if the crosswalk actually removed them to prevent duplicates. + if (!bitstreamPolicies.isEmpty()) { + List remainingSubmissionPolicies = + authorizeService.findPoliciesByDSOAndType(context, bitstream, ResourcePolicy.TYPE_SUBMISSION); + if (remainingSubmissionPolicies.isEmpty()) { + authorizeService.addPolicies(context, bitstreamPolicies, bitstream); + } + } + // is this the primary bitstream? if (primaryID != null && mfileID.equals(primaryID)) { bundle.setPrimaryBitstreamID(bitstream); @@ -1301,7 +1333,7 @@ public String getObjectHandle(METSManifest manifest) * zip) * @param params Parameters passed to METSIngester * @param path the File path (either path in Zip package or a URL) - * @return the InputStream for the file + * @return the InputStream for the file, or NullInputStream if a zero-sized entry is encountered * @throws MetadataValidationException if validation error * @throws IOException if IO error */ @@ -1334,12 +1366,13 @@ protected static InputStream getFileInputStream(File pkgFile, // Retrieve the manifest file entry by name ZipEntry manifestEntry = zipPackage.getEntry(path); - // Get inputStream associated with this file - if (manifestEntry != null) { + if (manifestEntry.getSize() > 0) { + // Get inputStream associated with this file return zipPackage.getInputStream(manifestEntry); } else { - throw new MetadataValidationException("Manifest file references file '" - + path + "' not included in the zip."); + log.warn("Zero-sized file entry={} found in zip file={}. Returning empty InputStream.", + path, pkgFile); + return new NullInputStream(); } } } diff --git a/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java b/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java index 15e9f1b14494..48901b5ce5c4 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java @@ -310,8 +310,8 @@ protected void writeGroup(Context context, DSpaceObject relatedObject, Group gro for (EPerson member : group.getMembers()) { writer.writeEmptyElement(MEMBER); writer.writeAttribute(ID, String.valueOf(member.getID())); - if (null != member.getName()) { - writer.writeAttribute(NAME, member.getName()); + if (null != member.getEmail()) { + writer.writeAttribute(NAME, member.getEmail()); } } writer.writeEndElement(); diff --git a/dspace-api/src/main/java/org/dspace/content/service/BundleService.java b/dspace-api/src/main/java/org/dspace/content/service/BundleService.java index 10d6613b2a22..31e0d79f4907 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/BundleService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/BundleService.java @@ -146,4 +146,12 @@ public void moveBitstreamToBundle(Context context, Bundle targetBundle, Bitstre public void setOrder(Context context, Bundle bundle, UUID bitstreamIds[]) throws AuthorizeException, SQLException; int countTotal(Context context) throws SQLException; + + /** + * Returns the count of bitstreams for the given bundle, performance optimized. + * + * @param context DSpace Context + * @param bundle the bitstream bundle + */ + int countBitstreams(Context context, Bundle bundle) throws SQLException; } diff --git a/dspace-api/src/main/java/org/dspace/content/service/CollectionService.java b/dspace-api/src/main/java/org/dspace/content/service/CollectionService.java index 3a865d9d63fd..5e81488bf1ce 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/CollectionService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/CollectionService.java @@ -327,6 +327,18 @@ public void canEdit(Context context, Collection collection, boolean useInheritan public List findAuthorized(Context context, Community community, int actionID) throws java.sql.SQLException; + /** + * return an array of collections that user has a given permission on + * + * @param context DSpace Context + * @param community (optional) restrict search to a community, else null + * @param actions Listo of the of the action ADD, READ, ADMIN, etc. + * @return Collection [] of collections with matching permissions + * @throws SQLException if database error + */ + public List findAuthorized(Context context, Community community, List actions) + throws java.sql.SQLException; + /** * * @param context DSpace Context @@ -379,11 +391,11 @@ Group createDefaultReadGroup(Context context, Collection collection, String type * NOTE: for better performance, this method retrieves its results from an * index (cache) and does not query the database directly. * This means that results may be stale or outdated until https://github.com/DSpace/DSpace/issues/2853 is resolved" - * + * + * @param context DSpace Context * @param q limit the returned collection to those with metadata values matching the query terms. * The terms are used to make also a prefix query on SOLR so it can be used to implement * an autosuggest feature over the collection name - * @param context DSpace Context * @param community parent community * @param entityType limit the returned collection to those related to given entity type * @param offset the position of the first result to return @@ -392,7 +404,7 @@ Group createDefaultReadGroup(Context context, Collection collection, String type * @throws SQLException if something goes wrong * @throws SearchServiceException if search error */ - public List findCollectionsWithSubmit(String q, Context context, Community community, + public List findCollectionsWithSubmit(Context context, String q, Community community, String entityType, int offset, int limit) throws SQLException, SearchServiceException; /** @@ -410,11 +422,10 @@ public List findCollectionsWithSubmit(String q, Context context, Com * @param offset the position of the first result to return * @param limit paging limit * @return discovery search result objects - * @throws SQLException if something goes wrong * @throws SearchServiceException if search error */ public List findCollectionsWithSubmit(String q, Context context, Community community, - int offset, int limit) throws SQLException, SearchServiceException; + int offset, int limit) throws SearchServiceException; /** * Retrieve the first collection in the community or its descending that support @@ -450,17 +461,17 @@ public Collection retrieveCollectionWithSubmitByEntityType(Context context, Item * and does not query the database directly. * This means that results may be stale or outdated until * https://github.com/DSpace/DSpace/issues/2853 is resolved." - * + * + * @param context DSpace Context * @param q limit the returned collection to those with metadata values matching the query terms. * The terms are used to make also a prefix query on SOLR so it can be used to implement * an autosuggest feature over the collection name - * @param context DSpace Context * @param community parent community * @return total collections found * @throws SQLException if something goes wrong * @throws SearchServiceException if search error */ - public int countCollectionsWithSubmit(String q, Context context, Community community) + public int countCollectionsWithSubmit(Context context, String q, Community community) throws SQLException, SearchServiceException; /** @@ -469,18 +480,18 @@ public int countCollectionsWithSubmit(String q, Context context, Community commu * and does not query the database directly. * This means that results may be stale or outdated until * https://github.com/DSpace/DSpace/issues/2853 is resolved." - * + * + * @param context DSpace Context * @param q limit the returned collection to those with metadata values matching the query terms. * The terms are used to make also a prefix query on SOLR so it can be used to implement * an autosuggest feature over the collection name - * @param context DSpace Context * @param community parent community * @param entityType limit the returned collection to those related to given entity type * @return total collections found * @throws SQLException if something goes wrong * @throws SearchServiceException if search error */ - public int countCollectionsWithSubmit(String q, Context context, Community community, String entityType) + public int countCollectionsWithSubmit(Context context, String q, Community community, String entityType) throws SQLException, SearchServiceException; /** 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 3fea75665bcb..7c202a4837f3 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 @@ -915,23 +915,23 @@ Iterator findByLastModifiedSince(Context context, Date last) /** * finds all items for which the current user has editing rights * @param context DSpace context object + * @param q search query * @param offset page offset * @param limit page size limit * @return list of items for which the current user has editing rights - * @throws SQLException * @throws SearchServiceException */ - List findItemsWithEdit(Context context, int offset, int limit) - throws SQLException, SearchServiceException; + List findItemsWithEdit(Context context, String q, int offset, int limit) + throws SearchServiceException; /** * counts all items for which the current user has editing rights * @param context DSpace context object + * @param q search query * @return list of items for which the current user has editing rights - * @throws SQLException * @throws SearchServiceException */ - int countItemsWithEdit(Context context) throws SQLException, SearchServiceException; + int countItemsWithEdit(Context context, String q) throws SearchServiceException; /** * Check if the supplied item is an inprogress submission diff --git a/dspace-api/src/main/java/org/dspace/core/AbstractHibernateDAO.java b/dspace-api/src/main/java/org/dspace/core/AbstractHibernateDAO.java index 3658a3c92305..e5e5c5c44a7a 100644 --- a/dspace-api/src/main/java/org/dspace/core/AbstractHibernateDAO.java +++ b/dspace-api/src/main/java/org/dspace/core/AbstractHibernateDAO.java @@ -468,6 +468,9 @@ public List findByX(Context context, Class clazz, Map equals, for (Map.Entry entry : equals.entrySet()) { criteria.where(criteriaBuilder.equal(root.get(entry.getKey()), entry.getValue())); } + + criteria.orderBy(criteriaBuilder.asc(root.get("id"))); + return executeCriteriaQuery(context, criteria, cacheable, maxResults, offset); } 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 74a48b3d82c9..f25c5b2a795a 100644 --- a/dspace-api/src/main/java/org/dspace/core/Email.java +++ b/dspace-api/src/main/java/org/dspace/core/Email.java @@ -22,7 +22,6 @@ import java.util.Date; import java.util.Enumeration; import java.util.List; -import java.util.Properties; import jakarta.activation.DataHandler; import jakarta.activation.DataSource; @@ -44,18 +43,16 @@ import org.apache.logging.log4j.Logger; import org.apache.velocity.Template; import org.apache.velocity.VelocityContext; -import org.apache.velocity.app.Velocity; import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.exception.MethodInvocationException; import org.apache.velocity.exception.ParseErrorException; import org.apache.velocity.exception.ResourceNotFoundException; -import org.apache.velocity.runtime.resource.loader.StringResourceLoader; import org.apache.velocity.runtime.resource.util.StringResourceRepository; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; /** - * Builder 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: @@ -72,7 +69,7 @@ * Apache Velocity. They may contain VTL directives and property * placeholders. *

- * {@link addArgument(string)} adds a property to the {@code params} array + * {@link #addArgument(Object)} adds a property to the {@code params} array * in the Velocity context, which can be used to replace placeholder tokens * in the message. These arguments are indexed by number in the order they were * added to the message. @@ -80,9 +77,9 @@ * The DSpace configuration properties are also available to templates as the * array {@code config}, indexed by name. Example: {@code ${config.get('dspace.name')}} *

- * Recipients and attachments may be added as needed. See {@link addRecipient}, - * {@link addAttachment(File, String)}, and - * {@link addAttachment(InputStream, String, String)}. + * Recipients and attachments may be added as needed. See {@link #addRecipient}, + * {@link #addAttachment(File, String)}, and + * {@link #addAttachment(InputStream, String, String)}. *

* Headers such as Subject may be supplied by the template, by defining them * using the VTL directive {@code #set()}. Only headers named in the DSpace @@ -125,8 +122,8 @@ * *

* There are two ways to load a message body. One can create an instance of - * {@link Email} and call {@link setContent} on it, passing the body as a String. Or - * one can use the static factory method {@link getEmail} to load a file by its + * {@link Email} and call {@link #setContent} on it, passing the body as a String. Or + * one can use the static factory method {@link #getEmail} to load a file by its * complete filesystem path. In either case the text will be loaded into a * Velocity template. * @@ -175,18 +172,6 @@ public class Email { /** Velocity template settings. */ private static final String RESOURCE_REPOSITORY_NAME = "Email"; - private static final Properties VELOCITY_PROPERTIES = new Properties(); - static { - VELOCITY_PROPERTIES.put(Velocity.RESOURCE_LOADERS, "string"); - VELOCITY_PROPERTIES.put("resource.loader.string.description", - "Velocity StringResource loader"); - VELOCITY_PROPERTIES.put("resource.loader.string.class", - StringResourceLoader.class.getName()); - VELOCITY_PROPERTIES.put("resource.loader.string.repository.name", - RESOURCE_REPOSITORY_NAME); - VELOCITY_PROPERTIES.put("resource.loader.string.repository.static", - "false"); - } /** Velocity template for a message body */ private Template template; @@ -208,6 +193,13 @@ public Email() { charset = null; } + /** + * Get configuration service + */ + private static ConfigurationService getConfigurationService() { + return DSpaceServicesFactory.getInstance().getConfigurationService(); + } + /** * Add a recipient. * @@ -230,7 +222,7 @@ public void setContent(String name, String content) { arguments.clear(); VelocityEngine templateEngine = new VelocityEngine(); - templateEngine.init(VELOCITY_PROPERTIES); + templateEngine.init(Utils.getSecureVelocityProperties(RESOURCE_REPOSITORY_NAME)); StringResourceRepository repo = (StringResourceRepository) templateEngine.getApplicationAttribute(RESOURCE_REPOSITORY_NAME); @@ -347,10 +339,7 @@ public void reset() { */ public void send() throws MessagingException, IOException { build(); - - ConfigurationService config - = DSpaceServicesFactory.getInstance().getConfigurationService(); - boolean disabled = config.getBooleanProperty("mail.server.disabled", false); + boolean disabled = getConfigurationService().getBooleanProperty("mail.server.disabled", false); if (disabled) { LOG.info(format(message, body)); } else { @@ -364,7 +353,7 @@ public void send() throws MessagingException, IOException { * {@code mail.message.headers} then that name and its value will be added * to the message's headers. * - *

"subject" is treated specially: if {@link setSubject()} has not been + *

"subject" is treated specially: if {@link #setSubject} has not been * 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. @@ -379,15 +368,12 @@ void build() throw new MessagingException("Email has no body"); } - ConfigurationService config - = DSpaceServicesFactory.getInstance().getConfigurationService(); - // Get the mail configuration properties - String from = config.getProperty("mail.from.address"); + String from = getConfigurationService().getProperty("mail.from.address"); // If no character set specified, attempt to retrieve a default if (charset == null) { - charset = config.getProperty("mail.charset"); + charset = getConfigurationService().getProperty("mail.charset"); } // Get session @@ -402,11 +388,13 @@ void build() new InternetAddress(recipient)); } // Get headers defined by the template. - String[] templateHeaders = config.getArrayProperty("mail.message.headers"); + String[] templateHeaders = getConfigurationService().getArrayProperty("mail.message.headers"); // Format the mail message body VelocityContext vctx = new VelocityContext(); - vctx.put("config", new UnmodifiableConfigurationService(config)); + // Pass a restricted (via configuration) list of resolved Configuration keys and values, for + // template lookup + vctx.put("config", Utils.getAllowedTemplateConfig()); vctx.put("params", Collections.unmodifiableList(arguments)); StringWriter writer = new StringWriter(); @@ -584,12 +572,10 @@ public static Email getEmail(String emailFile) * message is sent. */ public static void main(String[] args) { - ConfigurationService config - = DSpaceServicesFactory.getInstance().getConfigurationService(); - String to = config.getProperty("mail.admin"); + String to = getConfigurationService().getProperty("mail.admin"); String subject = "DSpace test email"; - String server = config.getProperty("mail.server"); - String url = config.getProperty("dspace.ui.url"); + String server = getConfigurationService().getProperty("mail.server"); + String url = getConfigurationService().getProperty("dspace.ui.url"); Email message; try { if (args.length <= 0) { @@ -607,7 +593,7 @@ public static void main(String[] args) { System.out.println(" - To: " + to); System.out.println(" - Subject: " + subject); System.out.println(" - Server: " + server); - boolean disabled = config.getBooleanProperty("mail.server.disabled", false); + boolean disabled = getConfigurationService().getBooleanProperty("mail.server.disabled", false); if (disabled) { System.err.println("\nError sending email:"); System.err.println(" - Error: cannot test email because mail.server.disabled is set to true"); @@ -709,31 +695,4 @@ public OutputStream getOutputStream() throws IOException { throw new IOException("Cannot write to this read-only resource"); } } - - /** - * Wrap ConfigurationService to prevent templates from modifying - * the configuration. - */ - public static class UnmodifiableConfigurationService { - private final ConfigurationService configurationService; - - /** - * Swallow an instance of ConfigurationService. - * - * @param cs the real instance, to be wrapped. - */ - public UnmodifiableConfigurationService(ConfigurationService cs) { - configurationService = cs; - } - - /** - * Look up a key in the actual ConfigurationService. - * - * @param key to be looked up in the DSpace configuration. - * @return whatever value ConfigurationService associates with {@code key}. - */ - public String get(String key) { - return configurationService.getProperty(key); - } - } } diff --git a/dspace-api/src/main/java/org/dspace/core/LDN.java b/dspace-api/src/main/java/org/dspace/core/LDN.java index 8ae5cddf5b4a..19be26c586c9 100644 --- a/dspace-api/src/main/java/org/dspace/core/LDN.java +++ b/dspace-api/src/main/java/org/dspace/core/LDN.java @@ -10,15 +10,15 @@ import static org.apache.commons.lang3.StringUtils.EMPTY; import java.io.BufferedReader; -import java.io.FileInputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringWriter; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Properties; import jakarta.mail.MessagingException; import org.apache.commons.lang3.StringUtils; @@ -26,15 +26,14 @@ import org.apache.logging.log4j.Logger; import org.apache.velocity.Template; import org.apache.velocity.VelocityContext; -import org.apache.velocity.app.Velocity; import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.exception.MethodInvocationException; import org.apache.velocity.exception.ParseErrorException; import org.apache.velocity.exception.ResourceNotFoundException; -import org.apache.velocity.runtime.resource.loader.StringResourceLoader; import org.apache.velocity.runtime.resource.util.StringResourceRepository; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; +import org.dspace.storage.secure.SecureFileAccess; /** * Class representing an LDN message json @@ -57,22 +56,17 @@ public class LDN { /** Velocity template settings. */ private static final String RESOURCE_REPOSITORY_NAME = "LDN"; - private static final Properties VELOCITY_PROPERTIES = new Properties(); - static { - VELOCITY_PROPERTIES.put(Velocity.RESOURCE_LOADERS, "string"); - VELOCITY_PROPERTIES.put("resource.loader.string.description", - "Velocity StringResource loader"); - VELOCITY_PROPERTIES.put("resource.loader.string.class", - StringResourceLoader.class.getName()); - VELOCITY_PROPERTIES.put("resource.loader.string.repository.name", - RESOURCE_REPOSITORY_NAME); - VELOCITY_PROPERTIES.put("resource.loader.string.repository.static", - "false"); - } /** Velocity template for the message*/ private Template template; + /** Allowed base directory for LDN messages / templates **/ + private static final ConfigurationService configurationService = + DSpaceServicesFactory.getInstance().getConfigurationService(); + private static final String dspaceDir = configurationService.getProperty("dspace.dir", "/dspace"); + private static final String[] DEFAULT_TEMPLATE_PATHS = new String[]{ + dspaceDir + File.separatorChar + "config" + File.separatorChar + "ldn"}; + /** * Create a new ldn message. */ @@ -112,14 +106,13 @@ public void addArgument(Object arg) { * @throws IOException if IO error */ public String generateLDNMessage() { - ConfigurationService config - = DSpaceServicesFactory.getInstance().getConfigurationService(); - VelocityEngine templateEngine = new VelocityEngine(); - templateEngine.init(VELOCITY_PROPERTIES); + templateEngine.init(Utils.getSecureVelocityProperties(RESOURCE_REPOSITORY_NAME)); VelocityContext vctx = new VelocityContext(); - vctx.put("config", new LDN.UnmodifiableConfigurationService(config)); + // Pass a restricted (via configuration) list of resolved Configuration keys and values, for + // template lookup + vctx.put("config", Utils.getAllowedTemplateConfig()); vctx.put("params", Collections.unmodifiableList(arguments)); if (null == template) { @@ -162,8 +155,16 @@ public String generateLDNMessage() { public static LDN getLDNMessage(String ldnMessageFile) throws IOException { StringBuilder contentBuffer = new StringBuilder(); + List allowedBasePaths = List.of( + Arrays.stream(configurationService + .getArrayProperty("ldn.template.path", DEFAULT_TEMPLATE_PATHS)) + .findFirst() + .orElseThrow(() -> new IOException("No LDN template path configured")) + ); + String ldnFilePath = SecureFileAccess.calculateAbsolutePathUsingBaseDir(ldnMessageFile, + allowedBasePaths.get(0)); try ( - InputStream is = new FileInputStream(ldnMessageFile); + InputStream is = SecureFileAccess.getInputStream(ldnFilePath, allowedBasePaths, "ldn"); InputStreamReader ir = new InputStreamReader(is, "UTF-8"); BufferedReader reader = new BufferedReader(ir); ) { @@ -182,31 +183,4 @@ public static LDN getLDNMessage(String ldnMessageFile) ldn.setContent(ldnMessageFile, contentBuffer.toString()); return ldn; } - - /** - * Wrap ConfigurationService to prevent templates from modifying - * the configuration. - */ - public static class UnmodifiableConfigurationService { - private final ConfigurationService configurationService; - - /** - * Swallow an instance of ConfigurationService. - * - * @param cs the real instance, to be wrapped. - */ - public UnmodifiableConfigurationService(ConfigurationService cs) { - configurationService = cs; - } - - /** - * Look up a key in the actual ConfigurationService. - * - * @param key to be looked up in the DSpace configuration. - * @return whatever value ConfigurationService associates with {@code key}. - */ - public String get(String key) { - return configurationService.getProperty(key); - } - } } diff --git a/dspace-api/src/main/java/org/dspace/core/LegacyPluginServiceImpl.java b/dspace-api/src/main/java/org/dspace/core/LegacyPluginServiceImpl.java index e92ea137f31f..e7c092e75c93 100644 --- a/dspace-api/src/main/java/org/dspace/core/LegacyPluginServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/core/LegacyPluginServiceImpl.java @@ -17,6 +17,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -219,10 +220,10 @@ private Object getAnonymousPlugin(String classname) // Map of named plugin classes, [intfc,name] -> class // Also contains intfc -> "marker" to mark when interface has been loaded. - private final Map namedPluginClasses = new HashMap<>(); + private final Map namedPluginClasses = new ConcurrentHashMap<>(); // load and cache configuration data for the given interface. - private void configureNamedPlugin(String iname) + private synchronized void configureNamedPlugin(String iname) throws ClassNotFoundException { int found = 0; @@ -307,11 +308,10 @@ private int installNamedConfigs(String iname, String classname, String names[]) int found = 0; for (int i = 0; i < names.length; ++i) { String key = iname + SEP + names[i]; - if (namedPluginClasses.containsKey(key)) { + String existing = namedPluginClasses.putIfAbsent(key, classname); + if (existing != null) { log.error("Name collision in named plugin, implementation class=\"" + classname + "\", name=\"" + names[i] + "\""); - } else { - namedPluginClasses.put(key, classname); } log.debug("Got Named Plugin, intfc=" + iname + ", name=" + names[i] + ", class=" + classname); ++found; 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 a1294c3317ce..63369528a6e3 100644 --- a/dspace-api/src/main/java/org/dspace/core/Utils.java +++ b/dspace-api/src/main/java/org/dspace/core/Utils.java @@ -29,16 +29,23 @@ import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; import java.util.Random; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import com.coverity.security.Escape; import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.StringSubstitutor; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.velocity.app.Velocity; +import org.apache.velocity.runtime.resource.loader.StringResourceLoader; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; @@ -101,6 +108,11 @@ public final class Utils { private static final Calendar outCal = GregorianCalendar.getInstance(); + // Allowed configuration properties to pass to Velocity templates (Email, LDN) + private static final String[] DEFAULT_ALLOWED_TEMPLATE_CONFIGS = { + "dspace.name", "dspace.shortname", "dspace.ui.url", + "mail.helpdesk", "mail.message.helpdesk.telephone", "mail.admin", "mail.admin.name"}; + /** * Private constructor */ @@ -507,4 +519,63 @@ public static String interpolateConfigsInString(String string) { return StringSubstitutor.replace(string, config.getProperties()); } + /** + * Get a list of allowed DSpace configuration property keys that will be exposed to Velocity templates + * (used in Email and LDN messages) as a simple Map of strings. + * @return Map of strings representing resolved configuration properties + */ + public static Map getAllowedTemplateConfig() { + // Pass a restricted (via configuration) list of resolved Configuration keys and values, for + // template lookup + ConfigurationService configurationService = + DSpaceServicesFactory.getInstance().getConfigurationService(); + List allowedConfigurationKeys = List.of(configurationService.getArrayProperty( + "message.templates.allowed-config", DEFAULT_ALLOWED_TEMPLATE_CONFIGS)); + return allowedConfigurationKeys.stream() + .map(key -> { + String value = configurationService.getProperty(key); + return value != null ? Map.entry(key, value) : null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue + )); + } + + /** + * Create and return a set of default, secure Velocity configuration properties. + * @see {@link LDN}, {@link Email} + * + * @param resourceRepositoryName the templating context e.g. "LDN", "Email" + * @returns secure Velocity configuration for use with templating + */ + public static Properties getSecureVelocityProperties(String resourceRepositoryName) { + Properties secureVelocityProperties = new Properties(); + // Basic Velocity configuration + secureVelocityProperties.setProperty(Velocity.RESOURCE_LOADERS, "string"); + secureVelocityProperties.setProperty("resource.loader.string.description", + "Velocity StringResource loader"); + secureVelocityProperties.setProperty("resource.loader.string.class", + StringResourceLoader.class.getName()); + secureVelocityProperties.setProperty("resource.loader.string.repository.name", + resourceRepositoryName); + secureVelocityProperties.setProperty("resource.loader.string.repository.static", + "false"); + // Set secure default introspection and class restriction handling in Velocity + secureVelocityProperties.setProperty("introspector.uberspect.class", + "org.apache.velocity.util.introspection.SecureUberspector"); + secureVelocityProperties.setProperty("introspector.restrict.classes", + "java.lang.Class,java.lang.Runtime,java.lang.System"); + secureVelocityProperties.setProperty( "introspector.restrict.packages", + "java.lang.reflect,java.io,java.nio"); + // Set strict mode if configured (default: false, as we've always treated null values as blanks) + if (DSpaceServicesFactory.getInstance().getConfigurationService() + .getBooleanProperty("message.templates.strict_mode", false)) { + secureVelocityProperties.setProperty("runtime.strict_mode.enable", "true"); + } + + return secureVelocityProperties; + } + } 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 020331842703..3c116df13bd2 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,12 +9,15 @@ import java.io.IOException; import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; 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.client.utils.URIBuilder; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.logging.log4j.Logger; import org.dspace.app.client.DSpaceHttpClientFactory; @@ -141,7 +144,8 @@ protected boolean checkURL(String url, StringBuilder results) { 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)); + URI uri = new URIBuilder(url).build(); + CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(uri)); 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 || @@ -153,6 +157,9 @@ protected int getResponseStatus(String url, int redirects) { } } return statusCode; + } catch (URISyntaxException e) { + log.error("Invalid URL: ", url, e); + return 0; } catch (IOException ioe) { // Must be a bad URL log.debug("Bad link: " + ioe.getMessage()); diff --git a/dspace-api/src/main/java/org/dspace/curate/AbstractCurationTask.java b/dspace-api/src/main/java/org/dspace/curate/AbstractCurationTask.java index fa16d2736953..4d59d46a8042 100644 --- a/dspace-api/src/main/java/org/dspace/curate/AbstractCurationTask.java +++ b/dspace-api/src/main/java/org/dspace/curate/AbstractCurationTask.java @@ -13,6 +13,10 @@ import java.util.List; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.app.util.factory.UtilServiceFactory; +import org.dspace.app.util.service.DSpaceObjectUtils; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.DSpaceObject; @@ -42,6 +46,8 @@ public abstract class AbstractCurationTask implements CurationTask { protected ItemService itemService; protected HandleService handleService; protected ConfigurationService configurationService; + protected DSpaceObjectUtils dspaceObjectUtils; + private static final Logger log = LogManager.getLogger(); @Override public void init(Curator curator, String taskId) throws IOException { @@ -51,6 +57,7 @@ public void init(Curator curator, String taskId) throws IOException { itemService = ContentServiceFactory.getInstance().getItemService(); handleService = HandleServiceFactory.getInstance().getHandleService(); configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + dspaceObjectUtils = UtilServiceFactory.getInstance().getDSpaceObjectUtils(); } @Override @@ -153,25 +160,13 @@ protected void performItem(Item item) throws SQLException, IOException { @Override public int perform(Context ctx, String id) throws IOException { - DSpaceObject dso = dereference(ctx, id); - return (dso != null) ? perform(dso) : Curator.CURATE_FAIL; - } - - /** - * Returns a DSpaceObject for passed identifier, if it exists - * - * @param ctx DSpace context - * @param id canonical id of object - * @return dso - * DSpace object, or null if no object with id exists - * @throws IOException if IO error - */ - protected DSpaceObject dereference(Context ctx, String id) throws IOException { + DSpaceObject dso = null; try { - return handleService.resolveToObject(ctx, id); + dso = dspaceObjectUtils.findDSpaceObject(ctx, id); } catch (SQLException sqlE) { throw new IOException(sqlE.getMessage(), sqlE); } + return (dso != null) ? perform(dso) : Curator.CURATE_FAIL; } /** 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 b894dcd85f03..35f6fbee6916 100644 --- a/dspace-api/src/main/java/org/dspace/curate/Curation.java +++ b/dspace-api/src/main/java/org/dspace/curate/Curation.java @@ -10,15 +10,18 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; -import java.io.FileReader; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintStream; import java.io.Writer; +import java.nio.charset.StandardCharsets; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -38,6 +41,7 @@ import org.dspace.handle.service.HandleService; import org.dspace.scripts.DSpaceRunnable; import org.dspace.services.factory.DSpaceServicesFactory; +import org.dspace.storage.secure.SecureFileAccess; import org.dspace.utils.DSpace; /** @@ -112,8 +116,16 @@ private void handleCurationTask(Curator curator) throws IOException, SQLExceptio } else if (commandLine.hasOption('T')) { // load taskFile BufferedReader reader = null; + // in this case, Curation CLI expects to calculate the -T parameter from the user's current working dir + String taskFilePath = SecureFileAccess.calculateAbsolutePathUsingCwd(this.taskFile); try { - reader = new BufferedReader(new FileReader(this.taskFile)); + String dspaceDir = DSpaceServicesFactory.getInstance() + .getConfigurationService().getProperty("dspace.dir"); + List allowedTaskFileBasePath = new ArrayList<>( + Arrays.asList(DSpaceServicesFactory.getInstance().getConfigurationService() + .getArrayProperty("curate.taskfile.base", new String[]{dspaceDir}))); + reader = SecureFileAccess.getBufferedReader(taskFilePath, allowedTaskFileBasePath, + "curation-taskfile", StandardCharsets.UTF_8); while ((taskName = reader.readLine()) != null) { if (verbose) { super.handler.logInfo("Adding task: " + taskName); @@ -189,12 +201,25 @@ private void endScript(long timeRun) throws SQLException { private Curator initCurator() throws FileNotFoundException { Curator curator = new Curator(handler); OutputStream reporterStream; + String dspaceDir = DSpaceServicesFactory.getInstance() + .getConfigurationService().getProperty("dspace.dir"); + List allowedReporterBasePaths = new ArrayList<>(Arrays.asList(DSpaceServicesFactory.getInstance() + .getConfigurationService().getArrayProperty("curate.reporter.base", + new String[]{dspaceDir + File.separatorChar + "log"}))); if (null == this.reporter) { - reporterStream = NullOutputStream.NULL_OUTPUT_STREAM; + reporterStream = NullOutputStream.INSTANCE; } else if ("-".equals(this.reporter)) { reporterStream = System.out; } else { - reporterStream = new PrintStream(this.reporter); + // Reporter param comes from CLI execution. Calculate abs path from user's current working dir + String reporterFilePath = SecureFileAccess.calculateAbsolutePathUsingCwd(this.reporter); + try { + reporterStream = new PrintStream( + SecureFileAccess.getOutputStream( + reporterFilePath, allowedReporterBasePaths, "curation-reporter")); + } catch (IOException e) { + throw new FileNotFoundException(e.getLocalizedMessage()); + } } Writer reportWriter = new OutputStreamWriter(reporterStream); curator.setReporter(reportWriter); diff --git a/dspace-api/src/main/java/org/dspace/curate/CurationCliScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/curate/CurationCliScriptConfiguration.java index eaa04f477829..925bd4f2d232 100644 --- a/dspace-api/src/main/java/org/dspace/curate/CurationCliScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/curate/CurationCliScriptConfiguration.java @@ -20,6 +20,10 @@ public Options getOptions() { options = super.getOptions(); options.addOption("e", "eperson", true, "email address of curating eperson"); options.getOption("e").setRequired(true); + options.addOption("r", "reporter", true, + "relative or absolute path to the desired report file. Use '-' to report to console. If absent, no " + + "reporting"); + options.addOption("T", "taskfile", true, "file containing curation task names"); return options; } } diff --git a/dspace-api/src/main/java/org/dspace/curate/CurationClientOptions.java b/dspace-api/src/main/java/org/dspace/curate/CurationClientOptions.java index 8ec0f14697c0..03ad2f34b230 100644 --- a/dspace-api/src/main/java/org/dspace/curate/CurationClientOptions.java +++ b/dspace-api/src/main/java/org/dspace/curate/CurationClientOptions.java @@ -31,7 +31,8 @@ public enum CurationClientOptions { /** * This method resolves the CommandLine parameters to figure out which action the curation script should perform * - * @param commandLine The relevant CommandLine for the curation script + * @param commandLine The relevant CommandLine for the curation script. Note that -T is passed only + * from CurationCliScriptConfig and is not accessible from UI processes * @return The curation option to be ran, parsed from the CommandLine */ protected static CurationClientOptions getClientOption(CommandLine commandLine) { @@ -54,14 +55,10 @@ protected static Options constructOptions() { Options options = new Options(); options.addOption("t", "task", true, "curation task name; options: " + getTaskOptions()); - options.addOption("T", "taskfile", true, "file containing curation task names"); options.addOption("i", "id", true, "Id (handle) of object to perform task on, or 'all' to perform on whole repository"); options.addOption("p", "parameter", true, "a task parameter 'NAME=VALUE'"); options.addOption("q", "queue", true, "name of task queue to process"); - options.addOption("r", "reporter", true, - "relative or absolute path to the desired report file. Use '-' to report to console. If absent, no " + - "reporting"); options.addOption("s", "scope", true, "transaction scope to impose: use 'object', 'curation', or 'open'. If absent, 'open' applies"); options.addOption("v", "verbose", false, "report activity to stdout"); diff --git a/dspace-api/src/main/java/org/dspace/curate/Curator.java b/dspace-api/src/main/java/org/dspace/curate/Curator.java index 4076fab51989..91f984b72e5b 100644 --- a/dspace-api/src/main/java/org/dspace/curate/Curator.java +++ b/dspace-api/src/main/java/org/dspace/curate/Curator.java @@ -18,6 +18,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.factory.UtilServiceFactory; +import org.dspace.app.util.service.DSpaceObjectUtils; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.DSpaceObject; @@ -90,6 +92,7 @@ public static enum TxScope { protected TaskResolver resolver = new TaskResolver(); protected TxScope txScope = TxScope.OPEN; protected CommunityService communityService; + protected DSpaceObjectUtils dspaceObjectUtils; protected ItemService itemService; protected HandleService handleService; protected DSpaceRunnableHandler handler; @@ -109,6 +112,7 @@ public Curator(DSpaceRunnableHandler handler) { */ public Curator() { communityService = ContentServiceFactory.getInstance().getCommunityService(); + dspaceObjectUtils = UtilServiceFactory.getInstance().getDSpaceObjectUtils(); itemService = ContentServiceFactory.getInstance().getItemService(); handleService = HandleServiceFactory.getInstance().getHandleService(); resolver = new TaskResolver(); @@ -248,7 +252,7 @@ public void curate(Context c, String id) throws IOException { //Save the context on current execution thread curationCtx.set(c); - DSpaceObject dso = handleService.resolveToObject(c, id); + DSpaceObject dso = dspaceObjectUtils.findDSpaceObject(c,id); if (dso != null) { curate(dso); } else { diff --git a/dspace-api/src/main/java/org/dspace/discovery/DiscoverQuery.java b/dspace-api/src/main/java/org/dspace/discovery/DiscoverQuery.java index e133ad0ed170..185c768a057b 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/DiscoverQuery.java +++ b/dspace-api/src/main/java/org/dspace/discovery/DiscoverQuery.java @@ -72,6 +72,14 @@ public enum SORT_ORDER { private String discoveryConfigurationName; + /** + * The required authorizations user should have for the objects returned by the query. + * The READ authorization (Constants.READ) is always required and does not need to be added here. + */ + private List requiredAuthorization; + + private boolean inheritAuthorizations = true; + public DiscoverQuery() { //Initialize all our lists this.filterQueries = new ArrayList<>(); @@ -83,6 +91,7 @@ public DiscoverQuery() { this.hitHighlighting = new HashMap<>(); //Use a linked hashmap since sometimes insertion order might matter this.properties = new LinkedHashMap<>(); + this.requiredAuthorization = new ArrayList<>(); } @@ -411,4 +420,54 @@ public String getDiscoveryConfigurationName() { public void setDiscoveryConfigurationName(String discoveryConfigurationName) { this.discoveryConfigurationName = discoveryConfigurationName; } + + /** + * Return the required authorization user should have for the objects returned by this query + * + * @return the required authorizations + */ + public List getRequiredAuthorizations() { + return requiredAuthorization; + } + + /** + * Add a required authorization user should have for the objects returned by this query. + * The READ authorization (Constants.READ) is always required and does not need to be added here. + * + * @param action + * the required action + */ + public void addRequiredAuthorization(int action) { + this.requiredAuthorization.add(action); + } + + /** + * Remove a required authorization user should have for the objects returned by this query + * + * @param authorizationAction + * the required action + */ + public void removeRequiredAuthorization(int authorizationAction) { + this.requiredAuthorization.removeIf(action -> action == authorizationAction); + } + + /** + * Return whether authorizations should be inherited from parent objects + * + * @return true if authorizations should be inherited, false otherwise + */ + public boolean isInheritAuthorizationsEnabled() { + return inheritAuthorizations; + } + + /** + * Set whether authorizations should be inherited from parent objects + * + * @param inheritAuthorizations + * true if authorizations should be inherited, false otherwise + */ + public void setInheritAuthorizations(boolean inheritAuthorizations) { + this.inheritAuthorizations = inheritAuthorizations; + } + } diff --git a/dspace-api/src/main/java/org/dspace/discovery/SearchService.java b/dspace-api/src/main/java/org/dspace/discovery/SearchService.java index cb945648e7cd..986b2d23de2a 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SearchService.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SearchService.java @@ -90,6 +90,8 @@ DiscoverFilterQuery toFilterQuery(Context context, String field, String operator List getRelatedItems(Context context, Item item, DiscoveryMoreLikeThisConfiguration moreLikeThisConfiguration); + String createLocationQueryForAdministrableDSOs(String epersonAndGroupClause); + /** * Method to create a Query that includes all * communities and collections a user may administrate. @@ -124,6 +126,15 @@ List getRelatedItems(Context context, Item item, */ String escapeQueryChars(String query); + /** + * Utility method to format an autocomplete query over a specific field. + * + * @param query to search for + * @param autocompleteField the field to use to autocomplete search, if null or empty no field is used + * @return the constructed solr query + */ + String formatAutoCompleteQuery(String query, String autocompleteField); + FacetYearRange getFacetYearRange(Context context, IndexableObject scope, DiscoverySearchFilterFacet facet, List filterQueries, DiscoverQuery parentQuery) throws SearchServiceException; 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 a5fa04b3dc04..88437ca7b8dc 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java @@ -18,6 +18,7 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.Date; @@ -596,6 +597,58 @@ protected boolean requiresIndexing(String uniqueId, Date lastModified) return reindexItem || !inIndex; } + /** + * Retrieves from Solr the list of administrable communities and collections for the + * current user based on a clause containing the e-person and group IDs. + * Builds and returns the "location" query part for these DSO's. + * + * @param epersonAndGroupClause A Solr filter clause containing one or more IDs combined with OR, + * e.g. {@code "eUUIDe1 OR gUUIDg2 OR gUUIDg3 OR ..."}. + * + * @return An empty string if no administrable DSO exists, or a string in the form + * {@code "location:(mUUID1 OR lUUID2 ... )"} when there are administrable DSO's. + */ + @Override + public String createLocationQueryForAdministrableDSOs(String epersonAndGroupClause) { + StringBuilder locationQuery = new StringBuilder(); + try { + + SolrQuery solrQuery = new SolrQuery(); + + String query = "*:*"; + solrQuery.setQuery(query); + solrQuery.addField(SearchUtils.RESOURCE_ID_FIELD); + solrQuery.addField(SearchUtils.RESOURCE_TYPE_FIELD); + solrQuery.addFilterQuery("(" + SearchUtils.RESOURCE_TYPE_FIELD + ":" + IndexableCommunity.TYPE + " OR " + + SearchUtils.RESOURCE_TYPE_FIELD + ":" + IndexableCollection.TYPE + ")"); + solrQuery.addFilterQuery("admin:(" + epersonAndGroupClause + ")"); + solrQuery.setRows(Integer.MAX_VALUE); + + QueryResponse solrQueryResponse = solrSearchCore.getSolr().query(solrQuery, + solrSearchCore.REQUEST_METHOD); + if (solrQueryResponse != null) { + List containerUUIDs = new ArrayList<>(); + for (SolrDocument doc : solrQueryResponse.getResults()) { + String type = (String) doc.getFieldValue(SearchUtils.RESOURCE_TYPE_FIELD); + String uniqueID = (String) doc.getFieldValue(SearchUtils.RESOURCE_ID_FIELD); + if (IndexableCommunity.TYPE.equals(type)) { + containerUUIDs.add("m" + uniqueID); + } else if (IndexableCollection.TYPE.equals(type)) { + containerUUIDs.add("l" + uniqueID); + } + } + if (!containerUUIDs.isEmpty()) { + locationQuery.append("location:("); + locationQuery.append(String.join(" OR ", containerUUIDs)); + return locationQuery.append(")").toString(); + } + } + } catch (Exception e) { + log.error("Failed to retrieve administrable communities and collections from Solr:", e); + } + return ""; + } + @Override public String createLocationQueryForAdministrableItems(Context context) throws SQLException { @@ -969,8 +1022,20 @@ protected SolrQuery resolveToSolrQuery(Context context, DiscoverQuery discoveryQ if (0 < discoveryQuery.getHitHighlightingFields().size()) { solrQuery.setHighlight(true); solrQuery.add(HighlightParams.USE_PHRASE_HIGHLIGHTER, Boolean.TRUE.toString()); + boolean escapeHTML = configurationService.getBooleanProperty("discovery.highlights.escape-html", true); + String[] renderHTMLForFields = + configurationService.getArrayProperty("discovery.highlights.html-allowed-fields"); for (DiscoverHitHighlightingField highlightingField : discoveryQuery.getHitHighlightingFields()) { solrQuery.addHighlightField(highlightingField.getField() + "_hl"); + boolean allowHTMLInField = Arrays.stream(renderHTMLForFields) + .anyMatch(field -> highlightingField.getField().matches(field)); + if (!escapeHTML || allowHTMLInField) { + solrQuery.add("f." + highlightingField.getField() + "_hl." + HighlightParams.METHOD, "original"); + } else { + solrQuery.add("f." + highlightingField.getField() + "_hl." + HighlightParams.METHOD, "unified"); + solrQuery.add("f." + highlightingField.getField() + "_hl." + HighlightParams.ENCODER, "html"); + } + solrQuery.add("f." + highlightingField.getField() + "_hl." + HighlightParams.FRAGSIZE, String.valueOf(highlightingField.getMaxChars())); solrQuery.add("f." + highlightingField.getField() + "_hl." + HighlightParams.SNIPPETS, @@ -1611,6 +1676,27 @@ public String escapeQueryChars(String query) { return ClientUtils.escapeQueryChars(query); } + /** + * Utility method to format an autocomplete query over a specific field. Combines the escaped query with a + * wildcard search over the specified {@code autocompleteField}. This field is typically non-tokenized and + * allows recovering searches containing spaces as a single value. + * + * @param query the user input to search for + * @param autocompleteField non-tokenized field used for wildcard autocomplete + * @return the constructed Solr query, or the original query if blank + */ + @Override + public String formatAutoCompleteQuery(String query, String autocompleteField) { + if (StringUtils.isNotBlank(query)) { + StringBuilder buildQuery = new StringBuilder(); + String escapedQuery = escapeQueryChars(query); + buildQuery.append("(").append(escapedQuery).append(" OR ").append(autocompleteField).append(":*") + .append(escapedQuery).append("*").append(")"); + return buildQuery.toString(); + } + return query; + } + @Override public FacetYearRange getFacetYearRange(Context context, IndexableObject scope, DiscoverySearchFilterFacet facet, List filterQueries, diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServicePrivateItemPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServicePrivateItemPlugin.java index db543141e13d..aab1176d5af5 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServicePrivateItemPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServicePrivateItemPlugin.java @@ -45,9 +45,8 @@ public void additionalSearchParameters(Context context, DiscoverQuery discoveryQ solrQuery.addFilterQuery("NOT(discoverable:false)"); return; } - if (!authorizeService.isCommunityAdmin(context) && !authorizeService.isCollectionAdmin(context)) { + if (!authorizeService.isComColAdmin(context)) { solrQuery.addFilterQuery("NOT(discoverable:false)"); - } } catch (SQLException ex) { log.error(LogHelper.getHeader(context, "Error looking up authorization rights of current user", diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceResourceRestrictionPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceResourceRestrictionPlugin.java index d19616a85e10..c16317771a64 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceResourceRestrictionPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceResourceRestrictionPlugin.java @@ -18,12 +18,8 @@ import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.AuthorizeService; import org.dspace.authorize.service.ResourcePolicyService; -import org.dspace.content.Collection; -import org.dspace.content.Community; import org.dspace.content.DSpaceObject; import org.dspace.content.InProgressSubmission; -import org.dspace.content.Item; -import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.CollectionService; import org.dspace.content.service.CommunityService; import org.dspace.core.Constants; @@ -36,14 +32,14 @@ import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.eperson.service.GroupService; -import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.xmlworkflow.storedcomponents.ClaimedTask; import org.dspace.xmlworkflow.storedcomponents.PoolTask; import org.springframework.beans.factory.annotation.Autowired; /** * Restriction plugin that ensures that indexes all the resource policies. - * When a search is performed extra filter queries are added to retrieve only results to which the user has READ access + * When a search is performed extra filter queries are added to retrieve only results to which the user has the + * required authorization. * * @author Kevin Van de Velde (kevin at atmire dot com) * @author Mark Diggory (markd at atmire dot com) @@ -64,6 +60,8 @@ public class SolrServiceResourceRestrictionPlugin implements SolrServiceIndexPlu protected GroupService groupService; @Autowired(required = true) protected ResourcePolicyService resourcePolicyService; + @Autowired + protected SearchService searchService; @Override public void additionalIndex(Context context, IndexableObject idxObj, SolrInputDocument document) { @@ -83,50 +81,32 @@ public void additionalIndex(Context context, IndexableObject idxObj, SolrInputDo } if (dso != null) { try { - List policies = authorizeService.getPoliciesActionFilter(context, dso, Constants.READ); - for (ResourcePolicy resourcePolicy : policies) { - if (resourcePolicyService.isDateValid(resourcePolicy)) { - String fieldValue; - if (resourcePolicy.getGroup() != null) { - //We have a group add it to the value - fieldValue = "g" + resourcePolicy.getGroup().getID(); - } else { - //We have an eperson add it to the value - fieldValue = "e" + resourcePolicy.getEPerson().getID(); - - } - - document.addField("read", fieldValue); - } - - //remove the policy from the cache to save memory - context.uncacheEntity(resourcePolicy); - } - // also index ADMIN policies as ADMIN permissions provides READ access - // going up through the hierarchy for communities, collections and items - while (dso != null) { - if (dso instanceof Community || dso instanceof Collection || dso instanceof Item) { - List policiesAdmin = authorizeService - .getPoliciesActionFilter(context, dso, Constants.ADMIN); - for (ResourcePolicy resourcePolicy : policiesAdmin) { - if (resourcePolicyService.isDateValid(resourcePolicy)) { - String fieldValue; - if (resourcePolicy.getGroup() != null) { - // We have a group add it to the value - fieldValue = "g" + resourcePolicy.getGroup().getID(); - } else { - // We have an eperson add it to the value - fieldValue = "e" + resourcePolicy.getEPerson().getID(); - } - document.addField("read", fieldValue); - document.addField("admin", fieldValue); + // Index read, submit, edit and admin permissions + int[] actionsToIndex = new int[] { Constants.READ, Constants.WRITE, Constants.ADD, Constants.ADMIN }; + + for (int action : actionsToIndex) { + String indexedActionName = getIndexedActionName(action); + List policies = authorizeService.getPoliciesActionFilter(context, dso, action); + for (ResourcePolicy resourcePolicy : policies) { + if (resourcePolicyService.isDateValid(resourcePolicy)) { + String fieldValue; + // Avoid NPE in cases where the policy does not have group or eperson + if (resourcePolicy.getGroup() == null && resourcePolicy.getEPerson() == null) { + continue; } - - // remove the policy from the cache to save memory - context.uncacheEntity(resourcePolicy); + if (resourcePolicy.getGroup() != null) { + //We have a group add it to the value + fieldValue = "g" + resourcePolicy.getGroup().getID(); + } else { + //We have an eperson add it to the value + fieldValue = "e" + resourcePolicy.getEPerson().getID(); + } + document.addField(indexedActionName, fieldValue); } + + //remove the policy from the cache to save memory + context.uncacheEntity(resourcePolicy); } - dso = ContentServiceFactory.getInstance().getDSpaceObjectService(dso).getParentObject(context, dso); } } catch (SQLException e) { log.error(LogHelper.getHeader(context, "Error while indexing resource policies", @@ -140,36 +120,66 @@ public void additionalIndex(Context context, IndexableObject idxObj, SolrInputDo public void additionalSearchParameters(Context context, DiscoverQuery discoveryQuery, SolrQuery solrQuery) { try { if (!authorizeService.isAdmin(context)) { - StringBuilder resourceQuery = new StringBuilder(); - //Always add the anonymous group id to the query - Group anonymousGroup = groupService.findByName(context, Group.ANONYMOUS); - String anonGroupId = ""; - if (anonymousGroup != null) { - anonGroupId = anonymousGroup.getID().toString(); - } - resourceQuery.append("read:(g" + anonGroupId); + EPerson currentUser = context.getCurrentUser(); + StringBuilder epersonAndGroupClause = new StringBuilder(); if (currentUser != null) { - resourceQuery.append(" OR e").append(currentUser.getID()); + epersonAndGroupClause.append("e").append(currentUser.getID()); } - //Retrieve all the groups the current user is a member of ! Set groups = groupService.allMemberGroupsSet(context, currentUser); for (Group group : groups) { - resourceQuery.append(" OR g").append(group.getID()); + if (!epersonAndGroupClause.isEmpty()) { + epersonAndGroupClause.append(" OR g").append(group.getID()); + } else { + epersonAndGroupClause.append("g").append(group.getID()); + } } - resourceQuery.append(")"); + StringBuilder resourceQuery = new StringBuilder(); - String locations = DSpaceServicesFactory.getInstance() - .getServiceManager() - .getServiceByName(SearchService.class.getName(), - SearchService.class) - .createLocationQueryForAdministrableItems(context); + List actions = discoveryQuery.getRequiredAuthorizations(); + /* + * The `actions` list specifies the permissions required beyond the default "read" permission. + * It should not include "read" because checking for "read" is always implicit. + * + * The query is constructed as follows: + * - If no actions are provided, it checks only for "read" or "admin" permissions. + * - If "admin" is in the `actions` list, it checks only for admin permissions. + * - Otherwise, it checks for both "read" and the other specified actions. + * + * The resulting query follows this structure: (read AND action) OR admin. + */ + if (actions.isEmpty()) { + // If no actions are included, we only check for read permissions + resourceQuery.append("(read:(").append(epersonAndGroupClause).append("))").append( " OR ") + .append("admin:(").append(epersonAndGroupClause).append(")"); + } else if (actions.contains(Constants.ADMIN)) { + // If the actions array contains the admin action, we only check for admin permissions + resourceQuery.append("admin:(").append(epersonAndGroupClause).append(")"); + } else { + // If the actions array contains other actions, we check for read permissions and the actions passed + resourceQuery.append("(read:(").append(epersonAndGroupClause).append(")"); + for (int action : actions) { + String actionName = getIndexedActionName(action); + resourceQuery.append(" AND ").append(actionName).append(":(").append(epersonAndGroupClause) + .append(")"); + } + resourceQuery.append(")"); + resourceQuery.append(" OR ").append("admin:(") + .append(epersonAndGroupClause).append(")"); + } - if (StringUtils.isNotBlank(locations)) { - resourceQuery.append(" OR "); - resourceQuery.append(locations); + // Add to the query the locations the user has administrative rights on to cover the cases of + // inherited permissions only if the inherit authorizations flag is enabled + if (discoveryQuery.isInheritAuthorizationsEnabled()) { + String locations = searchService + .createLocationQueryForAdministrableDSOs(epersonAndGroupClause.toString()); + + if (StringUtils.isNotBlank(locations)) { + resourceQuery.append(" OR "); + resourceQuery.append(locations); + } } solrQuery.addFilterQuery(resourceQuery.toString()); @@ -178,4 +188,26 @@ public void additionalSearchParameters(Context context, DiscoverQuery discoveryQ log.error(LogHelper.getHeader(context, "Error while adding resource policy information to query", ""), e); } } + + /** + * Get the action name used for solr indexing for the given action id + * + * @param action action id + * @return solr action name used for indexing + */ + private String getIndexedActionName(int action) { + + switch (action) { + case Constants.READ: + return "read"; + case Constants.WRITE: + return "edit"; + case Constants.ADD: + return "submit"; + case Constants.ADMIN: + return "admin"; + default: + return Constants.actionText[action].toLowerCase(); + } + } } diff --git a/dspace-api/src/main/java/org/dspace/discovery/indexobject/CommunityIndexFactoryImpl.java b/dspace-api/src/main/java/org/dspace/discovery/indexobject/CommunityIndexFactoryImpl.java index e92819601839..9f4b558cce76 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/indexobject/CommunityIndexFactoryImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/indexobject/CommunityIndexFactoryImpl.java @@ -126,7 +126,7 @@ public List getLocations(Context context, IndexableCommunity indexableDS final Community target = indexableDSpaceObject.getIndexedObject(); List locations = new ArrayList<>(); // build list of community ids - List communities = target.getParentCommunities(); + List communities = communityService.getAllParents(context, target); // now put those into strings for (Community community : communities) { diff --git a/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java b/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java index 1aa31d4db9e5..bf9f7b9d6a01 100644 --- a/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java @@ -142,8 +142,8 @@ public void afterPropertiesSet() throws Exception { //Load enabled collections String[] citationEnabledCollections = configurationService - .getArrayProperty("citation-page.enabled_collections"); - citationEnabledCollectionsList = Arrays.asList(citationEnabledCollections); + .getArrayProperty("citation-page.enabled_collections"); + citationEnabledCollectionsList = new ArrayList(Arrays.asList(citationEnabledCollections)); //Load enabled communities, and add to collection-list String[] citationEnabledCommunities = configurationService diff --git a/dspace-api/src/main/java/org/dspace/embargo/DefaultEmbargoSetter.java b/dspace-api/src/main/java/org/dspace/embargo/DefaultEmbargoSetter.java index 7857a45eb8d5..265ec213da60 100644 --- a/dspace-api/src/main/java/org/dspace/embargo/DefaultEmbargoSetter.java +++ b/dspace-api/src/main/java/org/dspace/embargo/DefaultEmbargoSetter.java @@ -94,7 +94,6 @@ public void setEmbargo(Context context, Item item) if (!(bnn.equals(Constants.LICENSE_BUNDLE_NAME) || bnn.equals(Constants.METADATA_BUNDLE_NAME) || bnn .equals(CreativeCommonsServiceImpl.CC_BUNDLE_NAME))) { //AuthorizeManager.removePoliciesActionFilter(context, bn, Constants.READ); - generatePolicies(context, liftDate.toDate(), null, bn, item.getOwningCollection()); for (Bitstream bs : bn.getBitstreams()) { //AuthorizeManager.removePoliciesActionFilter(context, bs, Constants.READ); generatePolicies(context, liftDate.toDate(), null, bs, item.getOwningCollection()); diff --git a/dspace-api/src/main/java/org/dspace/eperson/EPerson.java b/dspace-api/src/main/java/org/dspace/eperson/EPerson.java index 996fc96e3aa8..6f8e1043f9ec 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/EPerson.java +++ b/dspace-api/src/main/java/org/dspace/eperson/EPerson.java @@ -369,7 +369,7 @@ public int getType() { @Override public String getName() { - return getEmail(); + return this.getFullName(); } String getDigestAlgorithm() { diff --git a/dspace-api/src/main/java/org/dspace/eperson/EPersonCLITool.java b/dspace-api/src/main/java/org/dspace/eperson/EPersonCLITool.java index 343ddcccfa39..53451e07a9ec 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/EPersonCLITool.java +++ b/dspace-api/src/main/java/org/dspace/eperson/EPersonCLITool.java @@ -50,7 +50,7 @@ public class EPersonCLITool { private static final Option OPT_PHONE = new Option("t", "telephone", true, "telephone number, empty for none"); private static final Option OPT_LANGUAGE = new Option("l", "language", true, "the person's preferred language"); private static final Option OPT_REQUIRE_CERTIFICATE = new Option("c", "requireCertificate", true, - "if 'true', an X.509 certificate will be " + + "if 'true', a certificate will be " + "required for login"); private static final Option OPT_CAN_LOGIN = new Option("C", "canLogIn", true, "'true' if the user can log in"); diff --git a/dspace-api/src/main/java/org/dspace/eperson/Group.java b/dspace-api/src/main/java/org/dspace/eperson/Group.java index 24b44b8149a4..8146bf702d0a 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/Group.java +++ b/dspace-api/src/main/java/org/dspace/eperson/Group.java @@ -138,7 +138,7 @@ boolean contains(EPerson e) { return getMembers().contains(e); } - List getParentGroups() { + public List getParentGroups() { return parentGroups; } 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 4cec4c9c0d93..44727d3e5fb9 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/GroupServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/GroupServiceImpl.java @@ -142,6 +142,8 @@ public void addMember(Context context, Group group, EPerson e) { context.addEvent( new Event(Event.ADD, Constants.GROUP, group.getID(), Constants.EPERSON, e.getID(), e.getEmail(), getIdentifiers(context, group))); + log.info(LogHelper.getHeader(context, "add_group_eperson", + "group_id=" + group.getID() + ", eperson_id=" + e.getID())); } @Override @@ -157,6 +159,8 @@ public void addMember(Context context, Group groupParent, Group groupChild) thro context.addEvent(new Event(Event.ADD, Constants.GROUP, groupParent.getID(), Constants.GROUP, groupChild.getID(), groupChild.getName(), getIdentifiers(context, groupParent))); + log.info(LogHelper.getHeader(context, "add_group_subgroup", + "group_id=" + groupParent.getID() + ", subgroup_id=" + groupChild.getID())); } /** @@ -214,6 +218,8 @@ public void removeMember(Context context, Group group, EPerson ePerson) throws S if (group.remove(ePerson)) { context.addEvent(new Event(Event.REMOVE, Constants.GROUP, group.getID(), Constants.EPERSON, ePerson.getID(), ePerson.getEmail(), getIdentifiers(context, group))); + log.info(LogHelper.getHeader(context, "remove_group_eperson", + "group_id=" + group.getID() + ", eperson_id=" + ePerson.getID())); } } @@ -242,6 +248,8 @@ public void removeMember(Context context, Group groupParent, Group childGroup) t context.addEvent( new Event(Event.REMOVE, Constants.GROUP, groupParent.getID(), Constants.GROUP, childGroup.getID(), childGroup.getName(), getIdentifiers(context, groupParent))); + log.info(LogHelper.getHeader(context, "remove_group_subgroup", + "group_id=" + groupParent.getID() + ", subgroup_id=" + childGroup.getID())); } } diff --git a/dspace-api/src/main/java/org/dspace/eperson/dao/impl/GroupDAOImpl.java b/dspace-api/src/main/java/org/dspace/eperson/dao/impl/GroupDAOImpl.java index abd9fc830fa4..5c64af1c5097 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/dao/impl/GroupDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/dao/impl/GroupDAOImpl.java @@ -92,7 +92,7 @@ public List findAll(Context context, int pageSize, int offset) throws SQL @Override public List findByEPerson(Context context, EPerson ePerson) throws SQLException { Query query = createQuery(context, - "from Group where (from EPerson e where e.id = :eperson_id) in elements(epeople)"); + "select distinct g from Group g join g.epeople ep where ep.id = :eperson_id"); query.setParameter("eperson_id", ePerson.getID()); query.setHint("org.hibernate.cacheable", Boolean.TRUE); 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 87af01401ac0..f430959621fc 100644 --- a/dspace-api/src/main/java/org/dspace/external/OpenaireRestConnector.java +++ b/dspace-api/src/main/java/org/dspace/external/OpenaireRestConnector.java @@ -206,8 +206,11 @@ public InputStream get(String file, String accessToken) { break; } - // do not close this httpClient - result = getResponse.getEntity().getContent(); + // the client will be closed, we need to copy the response stream to a new one that we can return + try (InputStream is = getResponse.getEntity().getContent()) { + byte[] bytes = is.readAllBytes(); + result = new java.io.ByteArrayInputStream(bytes); + } } } catch (MalformedURLException e1) { getGotError(e1, url + '/' + file); diff --git a/dspace-api/src/main/java/org/dspace/external/OrcidConnectionException.java b/dspace-api/src/main/java/org/dspace/external/OrcidConnectionException.java new file mode 100644 index 000000000000..3574045aab2b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/external/OrcidConnectionException.java @@ -0,0 +1,33 @@ +/** + * 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; + +/** + * Exception thrown when there are issues with ORCID service connections. + * + * @author Boychuk Mykhaylo (mykhaylo.boychuk@4science.com) + */ +public class OrcidConnectionException extends Exception { + + private final int statusCode; + + public OrcidConnectionException(String message, int statusCode) { + super(message); + this.statusCode = statusCode; + } + + public OrcidConnectionException(String message, int statusCode, Throwable cause) { + super(message, cause); + this.statusCode = statusCode; + } + + public int getStatusCode() { + return statusCode; + } + +} 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 aa16af7a524d..3d61462cc354 100644 --- a/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java +++ b/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java @@ -9,10 +9,9 @@ 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.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; @@ -28,9 +27,6 @@ */ public class OrcidRestConnector { - /** - * log4j logger - */ private static final Logger log = LogManager.getLogger(OrcidRestConnector.class); private final String url; @@ -39,33 +35,33 @@ public OrcidRestConnector(String url) { this.url = url; } - public InputStream get(String path, String accessToken) { - CloseableHttpResponse getResponse = null; - InputStream result = null; - path = trimSlashes(path); - - String fullPath = url + '/' + path; + public InputStream get(String path, String accessToken) throws OrcidConnectionException { + String fullPath = url + '/' + trimSlashes(path); HttpGet httpGet = new HttpGet(fullPath); if (StringUtils.isNotBlank(accessToken)) { httpGet.addHeader("Content-Type", "application/vnd.orcid+xml"); httpGet.addHeader("Authorization","Bearer " + accessToken); } try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { - getResponse = httpClient.execute(httpGet); - 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); + try (CloseableHttpResponse httpResponse = httpClient.execute(httpGet)) { + if (!isSuccessful(httpResponse)) { + var statusCode = getStatusCode(httpResponse); + var reason = httpResponse.getStatusLine().getReasonPhrase(); + var error = String.format("The request failed with:%d code, reason:%s ", statusCode, reason); + throw new OrcidConnectionException(error, statusCode); + } + try (InputStream responseStream = httpResponse.getEntity().getContent()) { + // Read all the content of the response stream into a byte array to prevent TruncatedChunkException + byte[] content = responseStream.readAllBytes(); + return new ByteArrayInputStream(content); + } } + } catch (OrcidConnectionException e) { + throw e; } catch (Exception e) { - getGotError(e, fullPath); + log.error("Error in rest connector for path: " + fullPath, e); + throw new OrcidConnectionException("Failed to execute ORCID request: " + fullPath, 0, e); } - - return result; - } - - protected void getGotError(Exception e, String fullPath) { - log.error("Error in rest connector for path: " + fullPath, e); } public static String trimSlashes(String path) { @@ -78,8 +74,13 @@ public static String trimSlashes(String path) { return path; } - public static String convertStreamToString(InputStream is) { - Scanner s = new Scanner(is, StandardCharsets.UTF_8).useDelimiter("\\A"); - return s.hasNext() ? s.next() : ""; + private boolean isSuccessful(HttpResponse response) { + int statusCode = getStatusCode(response); + return statusCode >= 200 || statusCode <= 299; } -} + + private int getStatusCode(HttpResponse response) { + return response.getStatusLine().getStatusCode(); + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/OpenaireFundingDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/OpenaireFundingDataProvider.java index 62cef508c556..ae0f6c89a107 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/OpenaireFundingDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/OpenaireFundingDataProvider.java @@ -169,7 +169,19 @@ public int getNumberOfResults(String query) { String encodedQuery = encodeValue(query); Response projectResponse = connector.searchProjectByKeywords(0, 0, encodedQuery); - return Integer.parseInt(projectResponse.getHeader().getTotal()); + if (projectResponse == null || projectResponse.getHeader() == null) { + return 0; + } + String total = projectResponse.getHeader().getTotal(); + if (StringUtils.isBlank(total)) { + return 0; + } + try { + return Integer.parseInt(total); + } catch (NumberFormatException e) { + log.error("Failed to parse search result count from OpenAIRE: {}", e.getMessage()); + return 0; + } } /** 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 a9e10f92948d..fe2fdfa95381 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 @@ -21,6 +21,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.content.dto.MetadataValueDTO; +import org.dspace.external.OrcidConnectionException; import org.dspace.external.OrcidRestConnector; import org.dspace.external.model.ExternalDataObject; import org.dspace.external.provider.AbstractExternalDataProvider; @@ -89,6 +90,9 @@ public void init() throws IOException { public void initializeAccessToken() { // If we have reaches max retries or the access token is already set, return immediately if (maxClientRetries <= 0 || StringUtils.isNotBlank(accessToken)) { + if (maxClientRetries <= 0) { + log.warn("Maximum retry attempts reached for ORCID token retrieval"); + } return; } try { @@ -168,8 +172,14 @@ public Person getBio(String id) { return null; } initializeAccessToken(); - InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), accessToken); - return converter.convertSinglePerson(bioDocument); + try { + InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), + accessToken); + return converter.convertSinglePerson(bioDocument); + } catch (OrcidConnectionException e) { + log.error("Error retrieving ORCID bio for ID=" + id, e); + return null; + } } /** @@ -200,21 +210,26 @@ public List searchExternalDataObjects(String query, int star + "&start=" + start + "&rows=" + limit; log.debug("queryBio searchPath=" + searchPath + " accessToken=" + accessToken); - InputStream bioDocument = orcidRestConnector.get(searchPath, accessToken); - List results = converter.convert(bioDocument); - List bios = new LinkedList<>(); - for (Result result : results) { - OrcidIdentifier orcidIdentifier = result.getOrcidIdentifier(); - if (orcidIdentifier != null) { - log.debug("Found OrcidId=" + orcidIdentifier.getPath()); - String orcid = orcidIdentifier.getPath(); - Person bio = getBio(orcid); - if (bio != null) { - bios.add(bio); + try { + InputStream bioDocument = orcidRestConnector.get(searchPath, accessToken); + List results = converter.convert(bioDocument); + List bios = new LinkedList<>(); + for (Result result : results) { + OrcidIdentifier orcidIdentifier = result.getOrcidIdentifier(); + if (orcidIdentifier != null) { + log.debug("Found OrcidId=" + orcidIdentifier.getPath()); + String orcid = orcidIdentifier.getPath(); + Person bio = getBio(orcid); + if (bio != null) { + bios.add(bio); + } } } + return bios.stream().map(bio -> convertToExternalDataObject(bio)).collect(Collectors.toList()); + } catch (OrcidConnectionException e) { + log.error("Error searching ORCID for query=" + query, e); + return Collections.emptyList(); } - return bios.stream().map(bio -> convertToExternalDataObject(bio)).collect(Collectors.toList()); } @Override @@ -233,8 +248,13 @@ public int getNumberOfResults(String query) { + "&start=" + 0 + "&rows=" + 0; log.debug("queryBio searchPath=" + searchPath + " accessToken=" + accessToken); - InputStream bioDocument = orcidRestConnector.get(searchPath, accessToken); - return Math.min(converter.getNumberOfResultsFromXml(bioDocument), MAX_INDEX); + try { + InputStream bioDocument = orcidRestConnector.get(searchPath, accessToken); + return Math.min(converter.getNumberOfResultsFromXml(bioDocument), MAX_INDEX); + } catch (OrcidConnectionException e) { + log.error("Error getting number of results from ORCID for query=" + query, e); + return 0; + } } @@ -296,4 +316,4 @@ public void setOrcidRestConnector(OrcidRestConnector orcidRestConnector) { this.orcidRestConnector = orcidRestConnector; } -} +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/health/UserCheck.java b/dspace-api/src/main/java/org/dspace/health/UserCheck.java index 19a2a9ced355..875bb1bb0b60 100644 --- a/dspace-api/src/main/java/org/dspace/health/UserCheck.java +++ b/dspace-api/src/main/java/org/dspace/health/UserCheck.java @@ -50,26 +50,26 @@ public String run(ReportInfo ri) { info.put("Self registered", 0); for (EPerson e : epersons) { - if (e.getEmail() != null && e.getEmail().length() > 0) { + if (e.getEmail() != null && !e.getEmail().isEmpty()) { info.put("Have email", info.get("Have email") + 1); } if (e.canLogIn()) { info.put("Can log in (password)", info.get("Can log in (password)") + 1); } - if (e.getFirstName() != null && e.getFirstName().length() > 0) { + if (e.getFirstName() != null && !e.getFirstName().isEmpty()) { info.put("Have 1st name", info.get("Have 1st name") + 1); } - if (e.getLastName() != null && e.getLastName().length() > 0) { + if (e.getLastName() != null && !e.getLastName().isEmpty()) { info.put("Have 2nd name", info.get("Have 2nd name") + 1); } - if (e.getLanguage() != null && e.getLanguage().length() > 0) { + if (e.getLanguage() != null && !e.getLanguage().isEmpty()) { info.put("Have lang", info.get("Have lang") + 1); } - if (e.getNetid() != null && e.getNetid().length() > 0) { + if (e.getNetid() != null && !e.getNetid().isEmpty()) { info.put("Have netid", info.get("Have netid") + 1); } - if (e.getNetid() != null && e.getNetid().length() > 0) { + if (e.getNetid() != null && !e.getNetid().isEmpty()) { info.put("Self registered", info.get("Self registered") + 1); } } 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 99f1ee37a54e..d54c70c31984 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 @@ -95,6 +95,14 @@ private String prettifyAbstract(String abstractValue) { sb.append("\n"); } sb.append("\n"); + } else if (StringUtils.equals(nodeName, "jats:p")) { + NodeList secElements = childElement.getChildNodes(); + for (int j = 0; j < secElements.getLength(); j++) { + Node secChildElement = secElements.item(j); + sb.append(secChildElement.getTextContent()); + sb.append("\n"); + } + sb.append("\n"); } } 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 4ec1f4db39e7..60ed389383da 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 @@ -14,6 +14,7 @@ import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; @@ -29,7 +30,6 @@ import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpException; import org.apache.http.client.utils.URIBuilder; -import org.apache.jena.ext.xerces.impl.dv.util.Base64; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.app.util.XMLUtils; @@ -163,7 +163,7 @@ private Map> getLoginParams() { private Map getLoginHeaderParams() { Map params = new HashMap(); String authString = consumerKey + ":" + consumerSecret; - params.put("Authorization", "Basic " + Base64.encode(authString.getBytes())); + params.put("Authorization", "Basic " + Base64.getEncoder().encodeToString(authString.getBytes())); params.put("Content-type", "application/x-www-form-urlencoded"); return params; } diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/MetadatumDTO.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/MetadatumDTO.java index 265dd55eb933..b8a7507eaa61 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/MetadatumDTO.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/MetadatumDTO.java @@ -105,4 +105,13 @@ public String getValue() { public void setValue(String value) { this.value = value; } + + /** + * Return string representation of MetadatumDTO + * @return string representation of format "[schema].[element].[qualifier]=[value]" + */ + @Override + public String toString() { + return schema + "." + element + "." + qualifier + "=" + value; + } } diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ConditionalArrayElementAttributeProcessor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ConditionalArrayElementAttributeProcessor.java new file mode 100644 index 000000000000..f813a34c89b5 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ConditionalArrayElementAttributeProcessor.java @@ -0,0 +1,127 @@ +/** + * 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.contributor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * This Processor extracts values from a JSON array, but only when a condition + * on another attribute is met. For example, to extract all values of + * /names/value where /names/types contains "ror_display". + * + * Configurable via: + * pathToArray: e.g., /names + * elementAttribute: e.g., /value + * filterAttribute: e.g., /types + * requiredValueInFilter: e.g., ror_display + * + * Supports filtering when the filter attribute is either a JSON array or a single string. + * + * Example JSON: + * { + * "items": [{ + * "names": [ + * { "types": ["label", "ror_display"], "value": "Instituto Federal do Piauí" }, + * { "types": ["acronym"], "value": "IFPI" } + * ] + * }] + * } + * This processor can extract "Instituto Federal do Piauí" using proper configuration. + * + * Author: Jesiel (based on Mykhaylo Boychuk’s original processor) + */ +public class ConditionalArrayElementAttributeProcessor implements JsonPathMetadataProcessor { + + private static final Logger log = LogManager.getLogger(); + + private String pathToArray; + private String elementAttribute; + private String filterAttribute; + private String requiredValueInFilter; + + @Override + public Collection processMetadata(String json) { + JsonNode rootNode = convertStringJsonToJsonNode(json); + Collection results = new ArrayList<>(); + + if (rootNode == null) { + return results; + } + + Iterator array = rootNode.at(pathToArray).iterator(); + while (array.hasNext()) { + JsonNode element = array.next(); + JsonNode filterNode = element.at(filterAttribute); + + boolean match = false; + + if (filterNode.isArray()) { + for (JsonNode filterValue : filterNode) { + if (requiredValueInFilter.equalsIgnoreCase(filterValue.textValue())) { + match = true; + break; + } + } + } else if (filterNode.isTextual()) { + if (requiredValueInFilter.equalsIgnoreCase(filterNode.textValue())) { + match = true; + } + } + + if (match) { + JsonNode valueNode = element.at(elementAttribute); + if (valueNode.isTextual()) { + results.add(valueNode.textValue()); + } else if (valueNode.isArray()) { + for (JsonNode item : valueNode) { + if (item.isTextual() && StringUtils.isNotBlank(item.textValue())) { + results.add(item.textValue()); + } + } + } + } + } + + return results; + } + + private JsonNode convertStringJsonToJsonNode(String json) { + ObjectMapper mapper = new ObjectMapper(); + try { + return mapper.readTree(json); + } catch (JsonProcessingException e) { + log.error("Unable to process JSON response.", e); + return null; + } + } + + public void setPathToArray(String pathToArray) { + this.pathToArray = pathToArray; + } + + public void setElementAttribute(String elementAttribute) { + this.elementAttribute = elementAttribute; + } + + public void setFilterAttribute(String filterAttribute) { + this.filterAttribute = filterAttribute; + } + + public void setRequiredValueInFilter(String requiredValueInFilter) { + this.requiredValueInFilter = requiredValueInFilter; + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleJsonPathMetadataContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleJsonPathMetadataContributor.java index 590fc63283b9..db3ba16dc42c 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleJsonPathMetadataContributor.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleJsonPathMetadataContributor.java @@ -146,6 +146,9 @@ public Collection contributeMetadata(String fullJson) { } } for (String value : metadataValue) { + if (StringUtils.isBlank(value)) { + continue; + } MetadatumDTO metadatumDto = new MetadatumDTO(); metadatumDto.setValue(value); metadatumDto.setElement(field.getElement()); 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 c870161bf9bd..dc9954969394 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 @@ -55,6 +55,7 @@ public class PubmedImportMetadataSourceServiceImpl extends AbstractImportMetadat private String urlFetch; private String urlSearch; + private String apiKey; private int attempt = 3; @@ -210,6 +211,9 @@ public GetNbRecords(Query query) { @Override public Integer call() throws Exception { URIBuilder uriBuilder = new URIBuilder(urlSearch); + if (StringUtils.isNotBlank(apiKey)) { + uriBuilder.addParameter("api_key", apiKey); + } uriBuilder.addParameter("db", "pubmed"); uriBuilder.addParameter("term", query.getParameterAsClass("query", String.class)); Map> params = new HashMap>(); @@ -286,6 +290,9 @@ public Collection call() throws Exception { List records = new LinkedList(); URIBuilder uriBuilder = new URIBuilder(urlSearch); + if (StringUtils.isNotBlank(apiKey)) { + uriBuilder.addParameter("api_key", apiKey); + } uriBuilder.addParameter("db", "pubmed"); uriBuilder.addParameter("retstart", start.toString()); uriBuilder.addParameter("retmax", count.toString()); @@ -316,6 +323,9 @@ public Collection call() throws Exception { String webEnv = getSingleElementValue(response, "WebEnv"); URIBuilder uriBuilder2 = new URIBuilder(urlFetch); + if (StringUtils.isNotBlank(apiKey)) { + uriBuilder2.addParameter("api_key", apiKey); + } uriBuilder2.addParameter("db", "pubmed"); uriBuilder2.addParameter("retstart", start.toString()); uriBuilder2.addParameter("retmax", count.toString()); @@ -388,6 +398,9 @@ public GetRecord(Query q) { public ImportRecord call() throws Exception { URIBuilder uriBuilder = new URIBuilder(urlFetch); + if (StringUtils.isNotBlank(apiKey)) { + uriBuilder.addParameter("api_key", apiKey); + } uriBuilder.addParameter("db", "pubmed"); uriBuilder.addParameter("retmode", "xml"); uriBuilder.addParameter("id", query.getParameterAsClass("id", String.class)); @@ -428,6 +441,9 @@ public FindMatchingRecords(Query q) { public Collection call() throws Exception { URIBuilder uriBuilder = new URIBuilder(urlSearch); + if (StringUtils.isNotBlank(apiKey)) { + uriBuilder.addParameter("api_key", apiKey); + } uriBuilder.addParameter("db", "pubmed"); uriBuilder.addParameter("usehistory", "y"); uriBuilder.addParameter("term", query.getParameterAsClass("term", String.class)); @@ -457,6 +473,9 @@ public Collection call() throws Exception { String queryKey = getSingleElementValue(response, "QueryKey"); URIBuilder uriBuilder2 = new URIBuilder(urlFetch); + if (StringUtils.isNotBlank(apiKey)) { + uriBuilder.addParameter("api_key", apiKey); + } uriBuilder2.addParameter("db", "pubmed"); uriBuilder2.addParameter("retmode", "xml"); uriBuilder2.addParameter("WebEnv", webEnv); @@ -532,4 +551,8 @@ public void setUrlSearch(String urlSearch) { this.urlSearch = urlSearch; } + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + } 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 8298b6d6f011..4ee3404f7b9b 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 @@ -7,6 +7,8 @@ */ package org.dspace.importer.external.ror.service; +import static org.dspace.importer.external.liveimportclient.service.LiveImportClientImpl.HEADER_PARAMETERS; + import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; @@ -31,6 +33,7 @@ import org.dspace.importer.external.liveimportclient.service.LiveImportClient; import org.dspace.importer.external.service.AbstractImportMetadataSourceService; import org.dspace.importer.external.service.components.QuerySource; +import org.dspace.services.factory.DSpaceServicesFactory; import org.springframework.beans.factory.annotation.Autowired; /** @@ -43,10 +46,12 @@ public class RorImportMetadataSourceServiceImpl extends AbstractImportMetadataSo private final static Logger log = LogManager.getLogger(); protected static final String ROR_IDENTIFIER_PREFIX = "https://ror.org/"; + protected static final String ROR_CLIENT_ID_HEADER = "Client-Id"; + protected static final String ROR_CLIENT_ID_PROP = "ror.client-id"; private String url; - private int timeout = 1000; + private int timeout = 5000; @Autowired private LiveImportClient liveImportClient; @@ -160,7 +165,7 @@ public List call() throws Exception { * ROR query. This Callable uses as query value to ROR the string queryString * passed to constructor. If the object will be construct through {@code Query} * instance, the value of the Query's map with the key "query" will be used. - * + * * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) */ private class CountByQueryCallable implements Callable { @@ -189,7 +194,7 @@ public Integer call() throws Exception { */ public Integer count(String query) { try { - Map> params = new HashMap>(); + Map> params = getBaseParams(); URIBuilder uriBuilder = new URIBuilder(this.url); uriBuilder.addParameter("query", query); @@ -213,7 +218,7 @@ private List searchById(String id) { id = StringUtils.removeStart(id, ROR_IDENTIFIER_PREFIX); try { - Map> params = new HashMap>(); + Map> params = getBaseParams(); URIBuilder uriBuilder = new URIBuilder(this.url + "/" + id); @@ -234,7 +239,7 @@ private List searchById(String id) { private List search(String query) { List importResults = new ArrayList<>(); try { - Map> params = new HashMap>(); + Map> params = getBaseParams(); URIBuilder uriBuilder = new URIBuilder(this.url); uriBuilder.addParameter("query", query); @@ -261,6 +266,16 @@ private List search(String query) { return importResults; } + protected Map> getBaseParams() { + Map> params = new HashMap<>(); + String rorClientId = + DSpaceServicesFactory.getInstance().getConfigurationService().getProperty(ROR_CLIENT_ID_PROP); + if (StringUtils.isNotEmpty(rorClientId)) { + params.put(HEADER_PARAMETERS, Map.of(ROR_CLIENT_ID_HEADER, rorClientId)); + } + return params; + } + private JsonNode convertStringJsonToJsonNode(String json) { try { return new ObjectMapper().readTree(json); diff --git a/dspace-api/src/main/java/org/dspace/orcid/model/OrcidWorkFieldMapping.java b/dspace-api/src/main/java/org/dspace/orcid/model/OrcidWorkFieldMapping.java index 781a9dcbd904..faefe798e92b 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/model/OrcidWorkFieldMapping.java +++ b/dspace-api/src/main/java/org/dspace/orcid/model/OrcidWorkFieldMapping.java @@ -39,6 +39,7 @@ public class OrcidWorkFieldMapping { * The metadata fields related to the work external identifiers. */ private Map externalIdentifierFields = new HashMap<>(); + private Map> externalIdentifierPartOfMap = new HashMap<>(); /** * The metadata field related to the work publication date. @@ -129,6 +130,15 @@ public void setExternalIdentifierFields(String externalIdentifierFields) { this.externalIdentifierFields = parseConfigurations(externalIdentifierFields); } + public Map> getExternalIdentifierPartOfMap() { + return this.externalIdentifierPartOfMap; + } + + public void setExternalIdentifierPartOfMap( + HashMap> externalIdentifierPartOfMap) { + this.externalIdentifierPartOfMap = externalIdentifierPartOfMap; + } + public String getPublicationDateField() { return publicationDateField; } 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 ce68ab47c26e..f08aff740580 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,21 +7,29 @@ */ package org.dspace.orcid.model.factory; -import java.io.BufferedReader; +import static java.nio.charset.StandardCharsets.UTF_8; + import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; 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.json.JSONObject; +import org.json.JSONTokener; /** * Utility class for Orcid factory classes. This is used to parse the @@ -29,13 +37,12 @@ * contributors and external ids configuration). * * @author Luca Giamminonni (luca.giamminonni at 4science.it) - * */ public final class OrcidFactoryUtils { - private OrcidFactoryUtils() { + private static final Logger log = LogManager.getLogger(OrcidFactoryUtils.class); - } + private OrcidFactoryUtils() { } /** * Parse the given configurations value and returns a map with metadata fields @@ -46,7 +53,7 @@ private OrcidFactoryUtils() { * @return the configurations parsing result as map */ public static Map parseConfigurations(String configurations) { - Map configurationMap = new HashMap(); + Map configurationMap = new HashMap<>(); if (StringUtils.isBlank(configurations)) { return configurationMap; } @@ -55,7 +62,6 @@ public static Map parseConfigurations(String configurations) { String[] configurationSections = parseConfiguration(configuration); configurationMap.put(configurationSections[0], configurationSections[1]); } - return configurationMap; } @@ -87,37 +93,65 @@ private static String[] parseConfiguration(String configuration) { */ 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); + if (StringUtils.isBlank(clientSecret) || StringUtils.isBlank(clientId) || StringUtils.isBlank(oauthUrl)) { + String missingParams = (StringUtils.isBlank(clientId) ? "clientId " : "") + + (StringUtils.isBlank(clientSecret) ? "clientSecret " : "") + + (StringUtils.isBlank(oauthUrl) ? "oauthUrl" : ""); + log.error("Cannot retrieve ORCID access token: missing required parameters:{} ", missingParams.trim()); + return Optional.empty(); + } + + HttpPost httpPost = new HttpPost(oauthUrl); + + String auth = clientId + ":" + clientSecret; + String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(UTF_8)); + addHeaders(httpPost, encodedAuth); + + List params = new ArrayList<>(); + params.add(new BasicNameValuePair("grant_type", "client_credentials")); + params.add(new BasicNameValuePair("scope", "/read-public")); + httpPost.setEntity(new UrlEncodedFormEntity(params, UTF_8)); + + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { + log.debug("Sending ORCID token request to {}", oauthUrl); + HttpResponse response = httpClient.execute(httpPost); + if (!isSuccessful(response)) { + log.error("Failed to retrieve ORCID access token"); + return Optional.empty(); } - 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); - } - } + // Parsing JSON response + try (InputStream is = response.getEntity().getContent()) { + JSONObject responseObject = new JSONObject(new JSONTokener(is)); + if (responseObject.has("access_token")) { + String token = responseObject.getString("access_token"); + log.debug("Successfully retrieved ORCID access token"); + return Optional.of(token); + } else { + log.error("ORCID response missing access_token field:{} ", responseObject); + return Optional.empty(); } } - if (responseObject != null && responseObject.has("access_token")) { - return Optional.of((String) responseObject.get("access_token")); - } } - // Return empty by default - return Optional.empty(); } + + private static void addHeaders(HttpPost httpPost, String encodedAuth) { + httpPost.addHeader("Authorization", "Basic " + encodedAuth); + httpPost.addHeader("Accept", "application/json"); + httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); + } + + private static boolean isSuccessful(HttpResponse response) { + if (response == null) { + log.error("ORCID API request failed: null response received"); + return false; + } + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode != 200) { + var errorMsg = "ORCID API request failed with status code {}: {}"; + log.error(errorMsg, statusCode, response.getStatusLine().getReasonPhrase()); + return false; + } + return true; + } + } diff --git a/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java b/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java index 47619b3c1d63..280a5ac2155f 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java +++ b/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java @@ -9,6 +9,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.orcid.jaxb.model.common.Relationship.PART_OF; import static org.orcid.jaxb.model.common.Relationship.SELF; import java.util.ArrayList; @@ -73,12 +74,12 @@ public OrcidEntityType getEntityType() { @Override public Activity createOrcidObject(Context context, Item item) { Work work = new Work(); + work.setWorkType(getWorkType(context, item)); work.setJournalTitle(getJournalTitle(context, item)); work.setWorkContributors(getWorkContributors(context, item)); work.setWorkTitle(getWorkTitle(context, item)); work.setPublicationDate(getPublicationDate(context, item)); - work.setWorkExternalIdentifiers(getWorkExternalIds(context, item)); - work.setWorkType(getWorkType(context, item)); + work.setWorkExternalIdentifiers(getWorkExternalIds(context, item, work)); work.setShortDescription(getShortDescription(context, item)); work.setLanguageCode(getLanguageCode(context, item)); work.setUrl(getUrl(context, item)); @@ -149,57 +150,65 @@ private PublicationDate getPublicationDate(Context context, Item item) { } /** - * Creates an instance of ExternalIDs from the metadata values of the given - * item, using the orcid.mapping.funding.external-ids configuration. + * Returns a list of external work IDs constructed in the org.orcid.jaxb + * ExternalIDs object */ - private ExternalIDs getWorkExternalIds(Context context, Item item) { - ExternalIDs externalIdentifiers = new ExternalIDs(); - externalIdentifiers.getExternalIdentifier().addAll(getWorkSelfExternalIds(context, item)); - return externalIdentifiers; + private ExternalIDs getWorkExternalIds(Context context, Item item, Work work) { + ExternalIDs externalIDs = new ExternalIDs(); + externalIDs.getExternalIdentifier().addAll(getWorkExternalIdList(context, item, work)); + return externalIDs; } /** * Creates a list of ExternalID, one for orcid.mapping.funding.external-ids - * value, taking the values from the given item. + * value, taking the values from the given item and work type. */ - private List getWorkSelfExternalIds(Context context, Item item) { + private List getWorkExternalIdList(Context context, Item item, Work work) { - List selfExternalIds = new ArrayList<>(); + List externalIds = new ArrayList<>(); Map externalIdentifierFields = fieldMapping.getExternalIdentifierFields(); if (externalIdentifierFields.containsKey(SIMPLE_HANDLE_PLACEHOLDER)) { String handleType = externalIdentifierFields.get(SIMPLE_HANDLE_PLACEHOLDER); - selfExternalIds.add(getExternalId(handleType, item.getHandle(), SELF)); + ExternalID handle = new ExternalID(); + handle.setType(handleType); + handle.setValue(item.getHandle()); + handle.setRelationship(SELF); + externalIds.add(handle); } + // Resolve work type, used to determine identifier relationship type + // For version / funding relationships, we might want to use more complex + // business rules than just "work and id type" + final String workType = (work != null && work.getWorkType() != null) ? + work.getWorkType().value() : WorkType.OTHER.value(); getMetadataValues(context, item, externalIdentifierFields.keySet()).stream() - .map(this::getSelfExternalId) - .forEach(selfExternalIds::add); + .map(metadataValue -> this.getExternalId(metadataValue, workType)) + .forEach(externalIds::add); - return selfExternalIds; - } - - /** - * Creates an instance of ExternalID taking the value from the given - * metadataValue. The type of the ExternalID is calculated using the - * orcid.mapping.funding.external-ids configuration. The relationship of the - * ExternalID is SELF. - */ - private ExternalID getSelfExternalId(MetadataValue metadataValue) { - Map externalIdentifierFields = fieldMapping.getExternalIdentifierFields(); - String metadataField = metadataValue.getMetadataField().toString('.'); - return getExternalId(externalIdentifierFields.get(metadataField), metadataValue.getValue(), SELF); + return externalIds; } /** * Creates an instance of ExternalID with the given type, value and * relationship. */ - private ExternalID getExternalId(String type, String value, Relationship relationship) { + private ExternalID getExternalId(MetadataValue metadataValue, String workType) { + Map externalIdentifierFields = fieldMapping.getExternalIdentifierFields(); + Map> externalIdentifierPartOfMap = fieldMapping.getExternalIdentifierPartOfMap(); + String metadataField = metadataValue.getMetadataField().toString('.'); + String identifierType = externalIdentifierFields.get(metadataField); + // Default relationship type is SELF, configuration can + // override to PART_OF based on identifier and work type + Relationship relationship = SELF; + if (externalIdentifierPartOfMap.containsKey(identifierType) + && externalIdentifierPartOfMap.get(identifierType).contains(workType)) { + relationship = PART_OF; + } ExternalID externalID = new ExternalID(); - externalID.setType(type); - externalID.setValue(value); + externalID.setType(identifierType); + externalID.setValue(metadataValue.getValue()); externalID.setRelationship(relationship); return externalID; } 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 261f8ef9a9f7..d69e8842f5a9 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 @@ -224,7 +224,7 @@ private List findAllEntitiesLinkableWith(Context context, Item profile, St return findRelationshipsByItem(context, profile).stream() .map(relationship -> getRelatedItem(relationship, profile)) - .filter(item -> entityType.equals(itemService.getEntityTypeLabel(item))) + .filter(item -> item.isArchived() && entityType.equals(itemService.getEntityTypeLabel(item))) .collect(Collectors.toList()); } diff --git a/dspace-api/src/main/java/org/dspace/profile/ResearcherProfileServiceImpl.java b/dspace-api/src/main/java/org/dspace/profile/ResearcherProfileServiceImpl.java index 5de1ffa4ac93..b2466b33b56e 100644 --- a/dspace-api/src/main/java/org/dspace/profile/ResearcherProfileServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/profile/ResearcherProfileServiceImpl.java @@ -283,6 +283,8 @@ private Item createProfileItem(Context context, EPerson ePerson, Collection coll itemService.addMetadata(context, item, "dc", "title", null, null, fullName); itemService.addMetadata(context, item, "person", "email", null, null, ePerson.getEmail()); itemService.addMetadata(context, item, "dspace", "object", "owner", null, fullName, id, CF_ACCEPTED); + itemService.addMetadata(context, item, "person", "familyName", null, null, ePerson.getLastName()); + itemService.addMetadata(context, item, "person", "givenName", null, null, ePerson.getFirstName()); item = installItemService.installItem(context, workspaceItem); diff --git a/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventServiceImpl.java b/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventServiceImpl.java index 98077a1c0c76..171cc4c31159 100644 --- a/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventServiceImpl.java @@ -131,7 +131,7 @@ protected SolrClient getSolr() { if (solr == null) { String solrService = DSpaceServicesFactory.getInstance().getConfigurationService() .getProperty("qaevents.solr.server", "http://localhost:8983/solr/qaevent"); - return new HttpSolrClient.Builder(solrService).build(); + solr = new HttpSolrClient.Builder(solrService).build(); } return solr; } diff --git a/dspace-api/src/main/java/org/dspace/scripts/DSpaceRunnable.java b/dspace-api/src/main/java/org/dspace/scripts/DSpaceRunnable.java index 2ea0a52d6e34..8f905e01511e 100644 --- a/dspace-api/src/main/java/org/dspace/scripts/DSpaceRunnable.java +++ b/dspace-api/src/main/java/org/dspace/scripts/DSpaceRunnable.java @@ -117,7 +117,7 @@ private void handleHelpCommandLine() { * @param args The primitive array of Strings representing the parameters * @throws ParseException If something goes wrong */ - private StepResult parse(String[] args) throws ParseException { + protected StepResult parse(String[] args) throws ParseException { commandLine = new DefaultParser().parse(getScriptConfiguration().getOptions(), args); setup(); return StepResult.Continue; diff --git a/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitle.java b/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitle.java index b745f0719cb7..f6f9aaa38e50 100644 --- a/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitle.java +++ b/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitle.java @@ -9,7 +9,6 @@ import org.dspace.text.filter.DecomposeDiactritics; import org.dspace.text.filter.LowerCaseAndTrim; -import org.dspace.text.filter.StandardInitialArticleWord; import org.dspace.text.filter.StripDiacritics; import org.dspace.text.filter.TextFilter; @@ -20,7 +19,7 @@ */ public class OrderFormatTitle extends AbstractTextFilterOFD { { - filters = new TextFilter[] {new StandardInitialArticleWord(), + filters = new TextFilter[] { new DecomposeDiactritics(), new StripDiacritics(), new LowerCaseAndTrim()}; diff --git a/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitleMarc21.java b/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitleMarc21.java index fa9ba297258a..9148ca2a988a 100644 --- a/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitleMarc21.java +++ b/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitleMarc21.java @@ -9,7 +9,6 @@ import org.dspace.text.filter.DecomposeDiactritics; import org.dspace.text.filter.LowerCaseAndTrim; -import org.dspace.text.filter.MARC21InitialArticleWord; import org.dspace.text.filter.StripDiacritics; import org.dspace.text.filter.StripLeadingNonAlphaNum; import org.dspace.text.filter.TextFilter; @@ -21,7 +20,7 @@ */ public class OrderFormatTitleMarc21 extends AbstractTextFilterOFD { { - filters = new TextFilter[] {new MARC21InitialArticleWord(), + filters = new TextFilter[] { new DecomposeDiactritics(), new StripDiacritics(), new StripLeadingNonAlphaNum(), 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 32de86744d13..5c3048085069 100644 --- a/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java @@ -28,6 +28,7 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.EnumSet; @@ -228,6 +229,10 @@ public void postView(DSpaceObject dspaceObject, HttpServletRequest request, throw new RuntimeException(e); } + if (dspaceObject instanceof Bitstream && !isBitstreamLoggable((Bitstream) dspaceObject)) { + return; + } + if (solr == null) { return; } @@ -275,6 +280,10 @@ public void postView(DSpaceObject dspaceObject, @Override public void postView(DSpaceObject dspaceObject, String ip, String userAgent, String xforwardedfor, EPerson currentUser, String referrer) { + if (dspaceObject instanceof Bitstream && !isBitstreamLoggable((Bitstream) dspaceObject)) { + return; + } + if (solr == null) { return; } @@ -1634,4 +1643,35 @@ public Object anonymizeIp(String ip) throws UnknownHostException { throw new UnknownHostException("unknown ip format"); } + + /** + * Checks if a given Bitstream's bundles are configured to be logged in Solr statistics. + * + * @param bitstream The bitstream to check. + * @return {@code true} if the bitstream event should be logged, {@code false} otherwise. + */ + private boolean isBitstreamLoggable(Bitstream bitstream) { + String[] allowedBundles = configurationService + .getArrayProperty("solr-statistics.query.filter.bundles"); + if (allowedBundles == null || allowedBundles.length == 0) { + return true; + } + List allowedBundlesList = Arrays.asList(allowedBundles); + try { + List actualBundles = bitstream.getBundles(); + if (actualBundles.isEmpty()) { + return true; + } + for (Bundle bundle : actualBundles) { + if (allowedBundlesList.contains(bundle.getName())) { + return true; + } + } + } catch (SQLException e) { + log.error("Error checking bitstream bundles for logging statistics for bitstream {}", + bitstream.getID(), e); + return true; + } + return false; + } } diff --git a/dspace-api/src/main/java/org/dspace/statistics/export/processor/ExportEventProcessor.java b/dspace-api/src/main/java/org/dspace/statistics/export/processor/ExportEventProcessor.java index 434de459bad9..44fc5f3dc9c5 100644 --- a/dspace-api/src/main/java/org/dspace/statistics/export/processor/ExportEventProcessor.java +++ b/dspace-api/src/main/java/org/dspace/statistics/export/processor/ExportEventProcessor.java @@ -136,9 +136,10 @@ protected String getBaseParameters(Item item) .append(URLEncoder.encode(clientUA, UTF_8)); String hostName = Utils.getHostName(configurationService.getProperty("dspace.ui.url")); + String oaiPrefix = configurationService.getProperty("oai.identifier.prefix"); data.append("&").append(URLEncoder.encode("rft.artnum", UTF_8)).append("="). - append(URLEncoder.encode("oai:" + hostName + ":" + item + append(URLEncoder.encode("oai:" + oaiPrefix + ":" + item .getHandle(), UTF_8)); data.append("&").append(URLEncoder.encode("rfr_dat", UTF_8)).append("=") .append(URLEncoder.encode(referer, UTF_8)); diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageServiceImpl.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageServiceImpl.java index 1ec42f51183c..43427dae3bbc 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageServiceImpl.java @@ -270,6 +270,13 @@ public void cleanup(boolean deleteDbRecords, boolean verbose) throws SQLExceptio continue; } + // Check whether the bitstore file should be kept before + // expunging the database record, because expunge() would + // make the bitstream entity stale for subsequent queries. + boolean isRegistered = isRegisteredBitstream(bitstream.getInternalId()); + boolean hasDuplicate = !bitstreamService + .findDuplicateInternalIdentifier(context, bitstream).isEmpty(); + if (deleteDbRecords) { log.debug("deleting db record"); if (verbose) { @@ -282,16 +289,15 @@ public void cleanup(boolean deleteDbRecords, boolean verbose) throws SQLExceptio bitstreamService.expunge(context, bitstream); } - if (isRegisteredBitstream(bitstream.getInternalId())) { + if (isRegistered) { context.uncacheEntity(bitstream); - continue; // do not delete registered bitstreams + continue; // do not delete registered bitstreams from the bitstore } - - // Since versioning allows for multiple bitstreams, check if the internal - // identifier isn't used on - // another place - if (bitstreamService.findDuplicateInternalIdentifier(context, bitstream).isEmpty()) { + // Since versioning allows for multiple bitstreams, only + // remove the file if no other bitstream shares this + // internal identifier + if (!hasDuplicate) { this.getStore(bitstream.getStoreNumber()).remove(bitstream); String message = ("Deleted bitstreamID " + bid + ", internalID " + bitstream.getInternalId()); @@ -402,7 +408,6 @@ public void migrate(Context context, Integer assetstoreSource, Integer assetstor while (allBitstreamsInSource.hasNext()) { Bitstream bitstream = allBitstreamsInSource.next(); - log.info("Copying bitstream:" + bitstream .getID() + " from assetstore[" + assetstoreSource + "] to assetstore[" + assetstoreDestination + "] " + "Name:" + bitstream @@ -424,13 +429,7 @@ public void migrate(Context context, Integer assetstoreSource, Integer assetstor //modulo if ((processedCounter % batchCommitSize) == 0) { log.info("Migration Commit Checkpoint: " + processedCounter); - // UMD Customization - // This change was provided to DSpace in Pull Request 10940 - // This customization markers can be removed once the - // application has been upgraded to a DSpace version containing - // the pull request. context.commit(); - // End UMD Customizaton } } 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 7f8e7fe9c648..a08af8104ccd 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 @@ -19,9 +19,11 @@ import java.util.List; import java.util.Map; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.content.Bitstream; import org.dspace.core.Utils; +import org.dspace.services.factory.DSpaceServicesFactory; /** * Native DSpace (or "Directory Scatter" if you prefer) asset store. @@ -252,17 +254,15 @@ protected File getFile(Bitstream bitstream) throws IOException { } File bitstreamFile = new File(bufFilename.toString()); Path normalizedPath = bitstreamFile.toPath().normalize(); - // UMD Customization - // An equivalent change was provided to DSpace in Pull Request 11347 - // This change can be removed when updating to a version of DSpace that - // includes that pull request. - if (!normalizedPath.startsWith(baseDir.getCanonicalPath())) { + String[] allowedAssetstoreRoots = DSpaceServicesFactory.getInstance().getConfigurationService() + .getArrayProperty("assetstore.allowed.roots", new String[]{}); + if (!normalizedPath.startsWith(baseDir.getCanonicalPath()) + && !StringUtils.startsWithAny(normalizedPath.toString(), allowedAssetstoreRoots)) { log.error("Bitstream path outside of assetstore root requested:" + "bitstream={}, path={}, assetstore={}", bitstream.getID(), normalizedPath, baseDir.getCanonicalPath()); throw new IOException("Illegal bitstream path constructed"); } - // End UMD Customization return bitstreamFile; } @@ -277,18 +277,4 @@ public File getBaseDir() { public void setBaseDir(File baseDir) { this.baseDir = baseDir; } - - // UMD Customization - // This customization was added to support migrating the asset store - // files in the Kubernetes "sandbox", "test" and "qa" namespaces to - // AWS S3 and can be removed after the AWS S3 migration is complete. - public boolean exists(Bitstream bitstream) - throws IOException { - File file = getFile(bitstream); - if (file == null) { - return false; - } - return file.exists(); - } - // End UMD Customization } diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java index 36456a8945ec..ac1f147f41b4 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java @@ -9,36 +9,20 @@ import static java.lang.String.valueOf; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.net.URI; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.UUID; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.function.Supplier; -import com.amazonaws.AmazonClientException; -import com.amazonaws.auth.AWSCredentials; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.regions.Region; -import com.amazonaws.regions.Regions; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; -import com.amazonaws.services.s3.model.AmazonS3Exception; -import com.amazonaws.services.s3.model.GetObjectRequest; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.transfer.Download; -import com.amazonaws.services.s3.transfer.TransferManager; -import com.amazonaws.services.s3.transfer.TransferManagerBuilder; -import com.amazonaws.services.s3.transfer.Upload; -import jakarta.validation.constraints.NotNull; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.HelpFormatter; @@ -47,7 +31,6 @@ import org.apache.commons.cli.ParseException; import org.apache.commons.io.output.NullOutputStream; import org.apache.commons.lang3.StringUtils; -import org.apache.http.HttpStatus; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.content.Bitstream; @@ -58,6 +41,19 @@ import org.dspace.storage.bitstore.service.BitstreamStorageService; import org.dspace.util.FunctionalUtils; import org.springframework.beans.factory.annotation.Autowired; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.core.async.AsyncRequestBody; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.http.HttpStatusCode; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.S3CrtAsyncClientBuilder; +import software.amazon.awssdk.services.s3.model.ChecksumAlgorithm; +import software.amazon.awssdk.services.s3.model.HeadObjectResponse; +import software.amazon.awssdk.services.s3.model.NoSuchBucketException; /** * Asset store using Amazon's Simple Storage Service (S3). @@ -66,7 +62,7 @@ * * @author Richard Rodgers, Peter Dietz * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) - * + * @author Mark Patton */ public class S3BitStoreService extends BaseBitStoreService { @@ -83,30 +79,23 @@ public class S3BitStoreService extends BaseBitStoreService { */ static final String CSA = "MD5"; - // These settings control the way an identifier is hashed into - // directory and file names - // - // With digitsPerLevel 2 and directoryLevels 3, an identifier - // like 12345678901234567890 turns into the relative name - // /12/34/56/12345678901234567890. - // - // You should not change these settings if you have data in the - // asset store, as the BitstreamStorageManager will be unable - // to find your existing data. - protected static final int digitsPerLevel = 2; - protected static final int directoryLevels = 3; - private boolean enabled = false; + /** + * Override AWS endpoint if not null + */ + private String endpoint = null; + private String awsAccessKey; private String awsSecretKey; private String awsRegionName; private boolean useRelativePath; - - /** - * The maximum size of individual chunk to download from S3 when a file is accessed. Default 5Mb - */ - private long bufferSize = 5 * 1024 * 1024; + private double targetThroughputGbps = 10.0; + private long minPartSizeBytes = 8 * 1024 * 1024L; + private ChecksumAlgorithm s3ChecksumAlgorithm = ChecksumAlgorithm.CRC32; + private Integer maxConcurrency; + private Double memoryUsageFactor; + private Long initialReadBufferSizeInBytes; /** * container for all the assets @@ -121,13 +110,7 @@ public class S3BitStoreService extends BaseBitStoreService { /** * S3 service */ - private AmazonS3 s3Service = null; - - /** - * S3 transfer manager - * this is reused between put calls to use less resources for multiple uploads - */ - private TransferManager tm = null; + private S3AsyncClient s3AsyncClient = null; private static final ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); @@ -135,18 +118,103 @@ public class S3BitStoreService extends BaseBitStoreService { /** * Utility method for generate AmazonS3 builder * - * @param regions wanted regions in client - * @param awsCredentials credentials of the client + * @param region wanted regions in client + * @param credentialsProvider credentials of the client + * @param endpoint custom AWS endpoint + * @param targetThroughput target throughput in Gbps + * @param minPartSize minimum part size in bytes + * @param concurrency maximum number of concurrent requests + * @param memoryUsageFactor factor to determine memory usage for read buffer size, as a percentage of max memory. + * @param initialReadBufferSizeInBytes initial read buffer size in bytes, + * used if memoryUsageFactor is not set or out of bounds * @return builder with the specified parameters */ - protected static Supplier amazonClientBuilderBy( - @NotNull Regions regions, - @NotNull AWSCredentials awsCredentials + protected static Supplier amazonClientBuilderBy( + Region region, + AwsCredentialsProvider credentialsProvider, + String endpoint, + double targetThroughput, + long minPartSize, + Integer concurrency, + Double memoryUsageFactor, + Long initialReadBufferSizeInBytes ) { - return () -> AmazonS3ClientBuilder.standard() - .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) - .withRegion(regions) - .build(); + return () -> { + S3CrtAsyncClientBuilder crtBuilder = + S3AsyncClient.crtBuilder() + .targetThroughputInGbps(targetThroughput) + .minimumPartSizeInBytes(minPartSize); + + if (credentialsProvider != null) { + crtBuilder.credentialsProvider(credentialsProvider); + } + + if (region != null) { + crtBuilder.region(region); + } + + if (StringUtils.isNotBlank(endpoint)) { + crtBuilder.endpointOverride(URI.create(endpoint)); + crtBuilder.forcePathStyle(true); + } + + if (memoryUsageFactor == null) { + log.warn("custom heuristic cannot be applied!"); + + if (initialReadBufferSizeInBytes != null) { + crtBuilder.initialReadBufferSizeInBytes(initialReadBufferSizeInBytes); + } + + if (concurrency != null) { + crtBuilder.maxConcurrency(concurrency); + } + + return crtBuilder.build(); + } + + int maxConcurrency; + if (concurrency == null) { + log.warn("maxConcurrency is not set, defaulting to number of available processors"); + maxConcurrency = Runtime.getRuntime().availableProcessors(); + } else { + maxConcurrency = concurrency; + } + + long maxMemory; + if (memoryUsageFactor <= 0 || memoryUsageFactor > 1) { + log.warn("memoryUsageFactor is not set or out of bounds (0,1], defaulting to 0.1"); + maxMemory = Math.round(Math.floor(Runtime.getRuntime().maxMemory() * 0.1)); + } else { + maxMemory = Math.round(Math.floor(Runtime.getRuntime().maxMemory() * memoryUsageFactor)); + } + + final long readBuffer = Math.round( + Math.floor((((double) maxMemory / maxConcurrency / minPartSize) - 1) * minPartSize) + ); + + final long maxReadBuffer; + if (readBuffer < minPartSize) { + log.warn( + "Calculated read buffer size is less than the minimum part size. Adjusting to minimum part " + + "size." + ); + maxReadBuffer = minPartSize; + maxConcurrency = Math.max((int) Math.floor((double) maxMemory / (maxReadBuffer + minPartSize)), 1); + log.warn("Adjusted maxConcurrency to {} to fit memory constraints.", maxConcurrency); + } else { + maxReadBuffer = readBuffer; + } + + log.info( + "Calculated read buffer size: {} bytes, max concurrency: {}, based on memory usage factor: {} " + + "and max memory: {} bytes", + maxReadBuffer, maxConcurrency, memoryUsageFactor, maxMemory + ); + + return crtBuilder.maxConcurrency(maxConcurrency) + .initialReadBufferSizeInBytes(maxReadBuffer) + .build(); + }; } public S3BitStoreService() {} @@ -154,10 +222,10 @@ public S3BitStoreService() {} /** * This constructor is used for test purpose. * - * @param s3Service AmazonS3 service + * @param s3AsyncClient AmazonS3 service */ - protected S3BitStoreService(AmazonS3 s3Service) { - this.s3Service = s3Service; + protected S3BitStoreService(S3AsyncClient s3AsyncClient) { + this.s3AsyncClient = s3AsyncClient; } @Override @@ -174,7 +242,6 @@ public boolean isEnabled() { */ @Override public void init() throws IOException { - if (this.isInitialized() || !this.isEnabled()) { return; } @@ -183,29 +250,37 @@ public void init() throws IOException { if (StringUtils.isNotBlank(getAwsAccessKey()) && StringUtils.isNotBlank(getAwsSecretKey())) { log.warn("Use local defined S3 credentials"); // region - Regions regions = Regions.DEFAULT_REGION; + Region region = Region.US_EAST_1; if (StringUtils.isNotBlank(awsRegionName)) { try { - regions = Regions.fromName(awsRegionName); + region = Region.of(awsRegionName); } catch (IllegalArgumentException e) { log.warn("Invalid aws_region: " + awsRegionName); } } + // init client - s3Service = FunctionalUtils.getDefaultOrBuild( - this.s3Service, + s3AsyncClient = FunctionalUtils.getDefaultOrBuild( + this.s3AsyncClient, amazonClientBuilderBy( - regions, - new BasicAWSCredentials(getAwsAccessKey(), getAwsSecretKey()) - ) - ); - log.warn("S3 Region set to: " + regions.getName()); + region, + StaticCredentialsProvider.create( + AwsBasicCredentials.create(getAwsAccessKey(), getAwsSecretKey()) + ), + endpoint, targetThroughputGbps, minPartSizeBytes, maxConcurrency, memoryUsageFactor, + initialReadBufferSizeInBytes + ) + ); + log.warn("S3 Region set to: " + region.id()); } else { log.info("Using a IAM role or aws environment credentials"); - s3Service = FunctionalUtils.getDefaultOrBuild( - this.s3Service, - AmazonS3ClientBuilder::defaultClient - ); + s3AsyncClient = FunctionalUtils.getDefaultOrBuild( + this.s3AsyncClient, + amazonClientBuilderBy( + null, null, endpoint, targetThroughputGbps, + minPartSizeBytes, maxConcurrency, memoryUsageFactor, initialReadBufferSizeInBytes + ) + ); } // bucket name @@ -216,13 +291,10 @@ public void init() throws IOException { log.warn("S3 BucketName is not configured, setting default: " + bucketName); } - try { - if (!s3Service.doesBucketExistV2(bucketName)) { - s3Service.createBucket(bucketName); - log.info("Creating new S3 Bucket: " + bucketName); - } - } catch (AmazonClientException e) { - throw new IOException(e); + + if (!doesBucketExist(bucketName)) { + s3AsyncClient.createBucket(r -> r.bucket(bucketName)).join(); + log.info("Creating new S3 Bucket: " + bucketName); } this.initialized = true; log.info("AWS S3 Assetstore ready to go! bucket:" + bucketName); @@ -230,13 +302,23 @@ public void init() throws IOException { this.initialized = false; log.error("Can't initialize this store!", e); } + } - log.info("AWS S3 Assetstore ready to go! bucket:" + bucketName); + /** + * @param bucketName + * @return whether or not the specified bucket exists + */ + public boolean doesBucketExist(String bucketName ) { + try { + s3AsyncClient.headBucket(r -> r.bucket(bucketName)).join(); + return true; + } catch (CompletionException ce) { + if (!(ce.getCause() instanceof NoSuchBucketException)) { + log.error("headBucket(" + bucketName + ")", ce.getCause()); + } - tm = FunctionalUtils.getDefaultOrBuild(tm, () -> TransferManagerBuilder.standard() - .withAlwaysCalculateMultipartMd5(true) - .withS3Client(s3Service) - .build()); + return false; + } } /** @@ -264,7 +346,15 @@ public InputStream get(Bitstream bitstream) throws IOException { if (isRegisteredBitstream(key)) { key = key.substring(REGISTERED_FLAG.length()); } - return new S3LazyInputStream(key, bufferSize, bitstream.getSizeBytes()); + + final String objectKey = key; + + try { + return s3AsyncClient.getObject(r -> r.bucket(bucketName).key(objectKey), + AsyncResponseTransformer.toBlockingInputStream()).join(); + } catch (CompletionException e) { + throw new IOException(e.getCause()); + } } /** @@ -281,44 +371,40 @@ public InputStream get(Bitstream bitstream) throws IOException { @Override public void put(Bitstream bitstream, InputStream in) throws IOException { String key = getFullKey(bitstream.getInternalId()); - //Copy istream to temp file, and send the file, with some metadata - File scratchFile = File.createTempFile(bitstream.getInternalId(), "s3bs"); - try ( - FileOutputStream fos = new FileOutputStream(scratchFile); - // Read through a digest input stream that will work out the MD5 - DigestInputStream dis = new DigestInputStream(in, MessageDigest.getInstance(CSA)); - ) { - Utils.bufferedCopy(dis, fos); - in.close(); + ExecutorService executor = Executors.newSingleThreadExecutor(); + + try (DigestInputStream dis = new DigestInputStream(in, MessageDigest.getInstance(CSA))) { + AsyncRequestBody body = AsyncRequestBody.fromInputStream(dis, null, executor); - Upload upload = tm.upload(bucketName, key, scratchFile); + s3AsyncClient.putObject(b -> b.bucket(bucketName).key(key).checksumAlgorithm(s3ChecksumAlgorithm), + body).join(); - upload.waitForUploadResult(); + bitstream.setSizeBytes(s3AsyncClient.headObject(r -> r.bucket(bucketName).key(key)) + .join().contentLength()); - bitstream.setSizeBytes(scratchFile.length()); // we cannot use the S3 ETAG here as it could be not a MD5 in case of multipart upload (large files) or if // the bucket is encrypted bitstream.setChecksum(Utils.toHex(dis.getMessageDigest().digest())); bitstream.setChecksumAlgorithm(CSA); - - } catch (AmazonClientException | IOException | InterruptedException e) { + } catch (CompletionException e) { + log.error("put(" + bitstream.getInternalId() + ", is)", e.getCause()); + throw new IOException(e.getCause()); + } catch (IOException e) { log.error("put(" + bitstream.getInternalId() + ", is)", e); throw new IOException(e); } catch (NoSuchAlgorithmException nsae) { // Should never happen log.warn("Caught NoSuchAlgorithmException", nsae); } finally { - if (!scratchFile.delete()) { - scratchFile.deleteOnExit(); - } + executor.shutdown(); + in.close(); } } /** * Obtain technical metadata about an asset in the asset store. * - * Checksum used is (ETag) hex encoded 128-bit MD5 digest of an object's content as calculated by Amazon S3 - * (Does not use getContentMD5, as that is 128-bit MD5 digest calculated on caller's side) + * The MD5 checksum is calculated locally because it is not supported by AWS. * * @param bitstream The asset to describe * @param attrs A List of desired metadata fields @@ -329,7 +415,6 @@ public void put(Bitstream bitstream, InputStream in) throws IOException { */ @Override public Map about(Bitstream bitstream, List attrs) throws IOException { - String key = getFullKey(bitstream.getInternalId()); // If this is a registered bitstream, strip the -R prefix before retrieving if (isRegisteredBitstream(key)) { @@ -339,20 +424,18 @@ public Map about(Bitstream bitstream, List attrs) throws Map metadata = new HashMap<>(); try { + final String objectKey = key; + HeadObjectResponse response = s3AsyncClient.headObject(r -> r.bucket(bucketName).key(objectKey)).join(); - ObjectMetadata objectMetadata = s3Service.getObjectMetadata(bucketName, key); - if (objectMetadata != null) { - putValueIfExistsKey(attrs, metadata, "size_bytes", objectMetadata.getContentLength()); - putValueIfExistsKey(attrs, metadata, "modified", valueOf(objectMetadata.getLastModified().getTime())); - } - + putValueIfExistsKey(attrs, metadata, "size_bytes", response.contentLength()); + putValueIfExistsKey(attrs, metadata, "modified", valueOf(response.lastModified().toEpochMilli())); putValueIfExistsKey(attrs, metadata, "checksum_algorithm", CSA); if (attrs.contains("checksum")) { try (InputStream in = get(bitstream); DigestInputStream dis = new DigestInputStream(in, MessageDigest.getInstance(CSA)) ) { - Utils.copy(dis, NullOutputStream.NULL_OUTPUT_STREAM); + Utils.copy(dis, NullOutputStream.INSTANCE); byte[] md5Digest = dis.getMessageDigest().digest(); metadata.put("checksum", Utils.toHex(md5Digest)); } catch (NoSuchAlgorithmException nsae) { @@ -362,15 +445,15 @@ public Map about(Bitstream bitstream, List attrs) throws } return metadata; - } catch (AmazonS3Exception e) { - if (e.getStatusCode() == HttpStatus.SC_NOT_FOUND) { + } catch (CompletionException e) { + if (e.getCause() instanceof AwsServiceException awsEx && + awsEx.statusCode() == HttpStatusCode.NOT_FOUND) { return metadata; } - } catch (AmazonClientException e) { + log.error("about(" + key + ", attrs)", e); throw new IOException(e); } - return metadata; } /** @@ -383,10 +466,10 @@ public Map about(Bitstream bitstream, List attrs) throws public void remove(Bitstream bitstream) throws IOException { String key = getFullKey(bitstream.getInternalId()); try { - s3Service.deleteObject(bucketName, key); - } catch (AmazonClientException e) { - log.error("remove(" + key + ")", e); - throw new IOException(e); + s3AsyncClient.deleteObject(r -> r.bucket(bucketName).key(key)).join(); + } catch (CompletionException e) { + log.error("remove(" + key + ")", e.getCause()); + throw new IOException(e.getCause()); } } @@ -497,6 +580,62 @@ public void setUseRelativePath(boolean useRelativePath) { this.useRelativePath = useRelativePath; } + public double getTargetThroughputGbps() { + return targetThroughputGbps; + } + + public void setTargetThroughputGbps(double targetThroughputGbps) { + this.targetThroughputGbps = targetThroughputGbps; + } + + public long getMinPartSizeBytes() { + return minPartSizeBytes; + } + + public void setMinPartSizeBytes(long minPartSizeBytes) { + this.minPartSizeBytes = minPartSizeBytes; + } + + public ChecksumAlgorithm getS3ChecksumAlgorithm() { + return s3ChecksumAlgorithm; + } + + public void setS3ChecksumAlgorithm(ChecksumAlgorithm s3ChecksumAlgorithm) { + this.s3ChecksumAlgorithm = s3ChecksumAlgorithm; + } + + public Integer getMaxConcurrency() { + return maxConcurrency; + } + + public void setMaxConcurrency(Integer maxConcurrency) { + this.maxConcurrency = maxConcurrency; + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public Double getMemoryUsageFactor() { + return memoryUsageFactor; + } + + public void setMemoryUsageFactor(Double memoryUsageFactor) { + this.memoryUsageFactor = memoryUsageFactor; + } + + public Long getInitialReadBufferSizeInBytes() { + return initialReadBufferSizeInBytes; + } + + public void setInitialReadBufferSizeInBytes(Long initialReadBufferSizeInBytes) { + this.initialReadBufferSizeInBytes = initialReadBufferSizeInBytes; + } + /** * Contains a command-line testing tool. Expects arguments: * -a accessKey -s secretKey -f assetFileName @@ -537,73 +676,18 @@ public static void main(String[] args) throws Exception { S3BitStoreService store = new S3BitStoreService(); - AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create(accessKey, secretKey)); - store.s3Service = AmazonS3ClientBuilder.standard() - .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) - .build(); - - //Todo configurable region - Region usEast1 = Region.getRegion(Regions.US_EAST_1); - store.s3Service.setRegion(usEast1); + // Todo configurable region + store.s3AsyncClient = S3AsyncClient.builder().credentialsProvider(credentialsProvider). + region(Region.US_EAST_1).build(); // get hostname of DSpace UI to use to name bucket String hostname = Utils.getHostName(configurationService.getProperty("dspace.ui.url")); //Bucketname should be lowercase store.bucketName = DEFAULT_BUCKET_PREFIX + hostname + ".s3test"; - store.s3Service.createBucket(store.bucketName); - /* Broken in DSpace 6 TODO Refactor - // time everything, todo, swtich to caliper - long start = System.currentTimeMillis(); - // Case 1: store a file - String id = store.generateId(); - System.out.print("put() file " + assetFile + " under ID " + id + ": "); - FileInputStream fis = new FileInputStream(assetFile); - //TODO create bitstream for assetfile... - Map attrs = store.put(fis, id); - long now = System.currentTimeMillis(); - System.out.println((now - start) + " msecs"); - start = now; - // examine the metadata returned - Iterator iter = attrs.keySet().iterator(); - System.out.println("Metadata after put():"); - while (iter.hasNext()) - { - String key = (String)iter.next(); - System.out.println( key + ": " + (String)attrs.get(key) ); - } - // Case 2: get metadata and compare - System.out.print("about() file with ID " + id + ": "); - Map attrs2 = store.about(id, attrs); - now = System.currentTimeMillis(); - System.out.println((now - start) + " msecs"); - start = now; - iter = attrs2.keySet().iterator(); - System.out.println("Metadata after about():"); - while (iter.hasNext()) - { - String key = (String)iter.next(); - System.out.println( key + ": " + (String)attrs.get(key) ); - } - // Case 3: retrieve asset and compare bits - System.out.print("get() file with ID " + id + ": "); - java.io.FileOutputStream fos = new java.io.FileOutputStream(assetFile+".echo"); - InputStream in = store.get(id); - Utils.bufferedCopy(in, fos); - fos.close(); - in.close(); - now = System.currentTimeMillis(); - System.out.println((now - start) + " msecs"); - start = now; - // Case 4: remove asset - System.out.print("remove() file with ID: " + id + ": "); - store.remove(id); - now = System.currentTimeMillis(); - System.out.println((now - start) + " msecs"); - System.out.flush(); - // should get nothing back now - will throw exception - store.get(id); -*/ + store.s3AsyncClient.createBucket(r -> r.bucket(store.bucketName)).join(); } /** @@ -614,85 +698,4 @@ public static void main(String[] args) throws Exception { public boolean isRegisteredBitstream(String internalId) { return internalId.startsWith(REGISTERED_FLAG); } - - public void setBufferSize(long bufferSize) { - this.bufferSize = bufferSize; - } - - /** - * This inner class represent an InputStream that uses temporary files to - * represent chunk of the object downloaded from S3. When the input stream is - * read the class look first to the current chunk and download a new one once if - * the current one as been fully read. The class is responsible to close a chunk - * as soon as a new one is retrieved, the last chunk is closed when the input - * stream itself is closed or the last byte is read (the first of the two) - */ - public class S3LazyInputStream extends InputStream { - private InputStream currentChunkStream; - private String objectKey; - private long endOfChunk = -1; - private long chunkMaxSize; - private long currPos = 0; - private long fileSize; - - public S3LazyInputStream(String objectKey, long chunkMaxSize, long fileSize) throws IOException { - this.objectKey = objectKey; - this.chunkMaxSize = chunkMaxSize; - this.endOfChunk = 0; - this.fileSize = fileSize; - downloadChunk(); - } - - @Override - public int read() throws IOException { - // is the current chunk completely read and other are available? - if (currPos == endOfChunk && currPos < fileSize) { - currentChunkStream.close(); - downloadChunk(); - } - - int byteRead = currPos < endOfChunk ? currentChunkStream.read() : -1; - // do we get any data or are we at the end of the file? - if (byteRead != -1) { - currPos++; - } else { - currentChunkStream.close(); - } - return byteRead; - } - - /** - * This method download the next chunk from S3 - * - * @throws IOException - * @throws FileNotFoundException - */ - private void downloadChunk() throws IOException, FileNotFoundException { - // Create a DownloadFileRequest with the desired byte range - long startByte = currPos; // Start byte (inclusive) - long endByte = Long.min(startByte + chunkMaxSize - 1, fileSize - 1); // End byte (inclusive) - GetObjectRequest getRequest = new GetObjectRequest(bucketName, objectKey) - .withRange(startByte, endByte); - - File currentChunkFile = File.createTempFile("s3-disk-copy-" + UUID.randomUUID(), "temp"); - currentChunkFile.deleteOnExit(); - try { - Download download = tm.download(getRequest, currentChunkFile); - download.waitForCompletion(); - currentChunkStream = new DeleteOnCloseFileInputStream(currentChunkFile); - endOfChunk = endOfChunk + download.getProgress().getBytesTransferred(); - } catch (AmazonClientException | InterruptedException e) { - currentChunkFile.delete(); - throw new IOException(e); - } - } - - @Override - public void close() throws IOException { - if (currentChunkStream != null) { - currentChunkStream.close(); - } - } - - } } diff --git a/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java b/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java new file mode 100644 index 000000000000..3d70831b0b9f --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java @@ -0,0 +1,169 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.storage.secure; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +/** + * Decent I/O path validation - not perfect when symlinks are used and we are writing + * as 'toRealPath' check on the resolved path fails for new files + * + * @author Kim Shepherd + */ +public final class SecureFileAccess { + + private SecureFileAccess() {} + + /** + * Validate a given path against an allowed base path. Does not attempt to calculate "real path" + * before validation, as this breaks for new files which don't yet exist. This can make the resulting + * validation still vulnerable to symlink traversal in some cases + * @param file the unvalidated file, usually derived from user input or configuration + * This MUST be an absolute path, and the caller is expected to calculate it based on best + * context (e.g. configured base path, CWD, dspace.dir, and so on) + * @param allowedBasePaths list of allowed base paths for this use case as per system configuration + * @param purpose the name of the calling component / use case for logging and inspection + * @throws IOException on validation failure + */ + public static Path validatePathForWrite(String file, List allowedBasePaths, String purpose) + throws IOException { + Path filePath = Path.of(file); + if (!filePath.isAbsolute()) { + throw new IOException("Absolute path required for I/O (%s): %s".formatted(purpose, file)); + } + for (String allowedBasePath : allowedBasePaths) { + Path basePath = Path.of(allowedBasePath) + .toRealPath() + .normalize(); + if (basePath == null) { + throw new IOException("Null base path can not be provided for validation"); + } + Path resolvedPath = basePath.resolve(file).normalize(); + if (resolvedPath.startsWith(basePath)) { + return resolvedPath; + } + } + + // If no valid path was resolved and returned by now + // we raise an exception and treat this as illegal access + throw new IOException("Illegal file path attempted for I/O (%s): %s".formatted(purpose, file)); + } + + /** + * Validate a given path against an allowed base path. + * More secure than the 'write' variant because we can explicitly resolve links as well. + * + * @param file the unvalidated file, usually derived from user input or configuration + * This MUST be an absolute path, and the caller is expected to calculate it based on best + * context (e.g. configured base path, CWD, dspace.dir, and so on) + * @param allowedBasePaths the allowed base paths for this use case as per system configuration + * @param purpose the name of the calling component / use case for logging and inspection + * @throws IOException on validation failure + */ + public static Path validatePathForRead(String file, List allowedBasePaths, String purpose) + throws IOException { + Path filePath = Path.of(file); + if (!filePath.isAbsolute()) { + throw new IOException("Absolute path required for I/O (%s): %s".formatted(purpose, file)); + } + for (String allowedBasePath : allowedBasePaths) { + Path basePath = Path.of(allowedBasePath) + .toRealPath() + .normalize(); + Path resolvedPath = basePath.resolve(file).toRealPath().normalize(); + if (resolvedPath.startsWith(basePath)) { + return resolvedPath; + } + } + // If no valid path was resolved and returned by now + // we raise an exception and treat this as illegal access + throw new IOException("Illegal file path attempted for I/O (%s): %s".formatted(purpose, file)); + } + + /** + * Get a buffered reader after validating file path. + * @param unvalidatedFile the unvalidated file, usually derived from user input or configuration + * @param allowedBasePaths the allowed base paths for this use case as per system configuration + * @param purpose the name of the calling component / use case for logging and inspection + * @throws IOException on validation failure + */ + public static BufferedReader getBufferedReader(String unvalidatedFile, List allowedBasePaths, + String purpose, Charset charset) throws IOException { + if (charset == null) { + charset = StandardCharsets.UTF_8; + } + Path validatedFile = validatePathForRead(unvalidatedFile, allowedBasePaths, purpose); + return Files.newBufferedReader(validatedFile, charset); + } + + /** + * Get an input stream after validating file path. + * @param unvalidatedFile the unvalidated file, usually derived from user input or configuration + * @param allowedBasePaths the allowed base paths for this use case as per system configuration + * @param purpose the name of the calling component / use case for logging and inspection + * @throws IOException on validation failure + */ + public static InputStream getInputStream(String unvalidatedFile, List allowedBasePaths, String purpose) + throws IOException { + Path validatedFile = validatePathForRead(unvalidatedFile, allowedBasePaths, purpose); + return Files.newInputStream(validatedFile); + + } + + /** + * Get an output stream after validating file path. New files can't use toRealPath() for link calculation so + * there is a bit of a trade-off in allowing some symlink traversal to occur + * @param unvalidatedFile the unvalidated file, usually derived from user input or configuration + * @param allowedBasePaths the allowed base paths for this use case as per system configuration + * @param purpose the name of the calling component / use case for logging and inspection + * @throws IOException on validation failure + */ + public static OutputStream getOutputStream(String unvalidatedFile, List allowedBasePaths, String purpose) + throws IOException { + Path validatedFile = validatePathForWrite(unvalidatedFile, allowedBasePaths, purpose); + return Files.newOutputStream(validatedFile); + } + + /** + * Calculate an absolute path (if not already absolute) using current working dir as a root + * for relative file paths + * @param file the relative or absolute file given as input + * @return absolute path calculated from file and cwd + */ + public static String calculateAbsolutePathUsingCwd(String file) { + String filePath = file; + Path path = Path.of(filePath); + if (!path.isAbsolute()) { + filePath = Path.of("").toAbsolutePath().resolve(path).normalize().toString(); + } + return filePath; + } + + /** + * Calculate an absolute path (if not already absolute) using a given base dir as a root + * for relative file paths + * @param file the relative or absolute file given as input + * @return absolute path calculated from file and base dir + */ + public static String calculateAbsolutePathUsingBaseDir(String file, String baseDir) { + String filePath = file; + Path path = Path.of(filePath); + if (!path.isAbsolute()) { + filePath = Path.of(baseDir).toAbsolutePath().resolve(path).normalize().toString(); + } + return filePath; + } +} diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationServiceImpl.java b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationServiceImpl.java index 8fb01cd36e92..c803f1407e05 100644 --- a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationServiceImpl.java @@ -67,6 +67,7 @@ public SubscriptionEmailNotificationServiceImpl(Map public void perform(Context context, DSpaceRunnableHandler handler, String subscriptionType, String frequency) { List communityItems = new ArrayList<>(); List collectionsItems = new ArrayList<>(); + EPerson currentEperson = context.getCurrentUser(); try { List subscriptions = findAllSubscriptionsBySubscriptionTypeAndFrequency(context, subscriptionType, frequency); @@ -77,7 +78,10 @@ public void perform(Context context, DSpaceRunnableHandler handler, String subsc for (Subscription subscription : subscriptions) { DSpaceObject dSpaceObject = subscription.getDSpaceObject(); EPerson ePerson = subscription.getEPerson(); - + // Set the current user to the subscribed eperson because the Solr query checks + // the permissions of the current user in the ANONYMOUS group. + // If there is no user (i.e., `current user = null`), it will send an email with no new items. + context.setCurrentUser(ePerson); if (!authorizeService.authorizeActionBoolean(context, ePerson, dSpaceObject, READ, true)) { iterator++; continue; @@ -126,6 +130,8 @@ public void perform(Context context, DSpaceRunnableHandler handler, String subsc handler.handleException(e); context.abort(); } + // Reset the current user because it was changed to subscriber eperson + context.setCurrentUser(currentEperson); } @SuppressWarnings("rawtypes") diff --git a/dspace-api/src/main/java/org/dspace/text/filter/InitialArticleWord.java b/dspace-api/src/main/java/org/dspace/text/filter/InitialArticleWord.java deleted file mode 100644 index 167b201e0f7a..000000000000 --- a/dspace-api/src/main/java/org/dspace/text/filter/InitialArticleWord.java +++ /dev/null @@ -1,172 +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.text.filter; - -/** - * Abstract class for implementing initial article word filters - * Allows you to create new classes with their own rules for mapping - * languages to article word lists. - * - * @author Graham Triggs - */ -public abstract class InitialArticleWord implements TextFilter { - /** - * When no language is passed, use null and let implementation decide what to do - */ - @Override - public String filter(String str) { - return filter(str, null); - } - - /** - * Do an initial definite/indefinite article filter on the passed string. - * On matching an initial word, can strip or move to the end, depending on the - * configuration of the implementing class. - * - * @param str The string to parse - * @param lang The language of the passed string - * @return String The filtered string - */ - @Override - public String filter(String str, String lang) { - // Get the list of article words for this language - String[] articleWordArr = getArticleWords(lang); - - // If we have an article word array, process the string - if (articleWordArr != null && articleWordArr.length > 0) { - String initialArticleWord = null; - int curPos = 0; - int initialStart = -1; - int initialEnd = -1; - - // Iterate through the characters until we find something significant, or hit the end - while (initialEnd < 0 && curPos < str.length()) { - // Have we found a significant character - if (Character.isLetterOrDigit(str.charAt(curPos))) { - // Mark this as the cut point for the initial word - initialStart = curPos; - - // Loop through the article words looking for a match - for (int idx = 0; initialEnd < 0 && idx < articleWordArr.length; idx++) { - // Extract a fragment from the string to test - // Must be same length as the article word - if (idx > 1 && initialArticleWord != null) { - // Only need to do so if we haven't already got one - // of the right length - if (initialArticleWord.length() != articleWordArr[idx].length()) { - initialArticleWord = extractText(str, curPos, articleWordArr[idx].length()); - } - } else { - initialArticleWord = extractText(str, curPos, articleWordArr[idx].length()); - } - - // Does the fragment match an article word? - if (initialArticleWord != null && initialArticleWord.equalsIgnoreCase(articleWordArr[idx])) { - // Check to see if the next character in the source - // is a whitespace - boolean isNextWhitespace = Character.isWhitespace( - str.charAt(curPos + articleWordArr[idx].length()) - ); - - // Check to see if the last character of the article word is a letter or digit - boolean endsLetterOrDigit = Character - .isLetterOrDigit(initialArticleWord.charAt(initialArticleWord.length() - 1)); - - // If the last character of the article word is a letter or digit, - // then it must be followed by whitespace, if not, it can be anything - // Setting endPos signifies that we have found an article word - if (endsLetterOrDigit && isNextWhitespace) { - initialEnd = curPos + initialArticleWord.length(); - } else if (!endsLetterOrDigit) { - initialEnd = curPos + initialArticleWord.length(); - } - } - } - - // Quit the loop, as we have a significant character - break; - } - - // Keep going - curPos++; - } - - // If endPos is positive, then we've found an article word - if (initialEnd > 0) { - // Find a cut point in the source string, removing any whitespace after the article word - int cutPos = initialEnd; - while (cutPos < str.length() && Character.isWhitespace(str.charAt(cutPos))) { - cutPos++; - } - - // Are we stripping the article word? - if (stripInitialArticle) { - // Yes, simply return everything after the cut - return str.substring(cutPos); - } else { - // No - move the initial article word to the end - return new StringBuilder(str.substring(cutPos)) - .append(wordSeparator) - .append(str.substring(initialStart, initialEnd)) - .toString(); - } - } - } - - // Didn't do any processing, or didn't find an initial article word - // Return the original string - return str; - } - - protected InitialArticleWord(boolean stripWord) { - this.wordSeparator = ", "; - stripInitialArticle = stripWord; - } - - protected InitialArticleWord() { - this.wordSeparator = ", "; - stripInitialArticle = false; - } - - /** - * Abstract method to get the list of words to use in the initial word filter - * - * @param lang The language to retrieve article words for - * @return An array of definite/indefinite article words - */ - protected abstract String[] getArticleWords(String lang); - // Separator to use when appending article to end - private final String wordSeparator; - - // Flag to signify initial article word should be removed - // If false, then the initial article word is appended to the end - private boolean stripInitialArticle = false; - - /** - * Helper method to extract text from a string. - * Ensures that there is significant data (ie. non-whitespace) - * after the segment requested. - * - * @param str - * @param pos - * @param len - * @return - */ - private String extractText(String str, int pos, int len) { - int testPos = pos + len; - while (testPos < str.length() && Character.isWhitespace(str.charAt(testPos))) { - testPos++; - } - - if (testPos < str.length()) { - return str.substring(pos, pos + len); - } - - return null; - } -} diff --git a/dspace-api/src/main/java/org/dspace/text/filter/Language.java b/dspace-api/src/main/java/org/dspace/text/filter/Language.java deleted file mode 100644 index 9be68d2ddfb9..000000000000 --- a/dspace-api/src/main/java/org/dspace/text/filter/Language.java +++ /dev/null @@ -1,142 +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.text.filter; - -import java.util.HashMap; -import java.util.Map; - -/** - * Define languages - both as IANA and ISO639-2 codes - * - * @author Graham Triggs - */ -public class Language { - public final String IANA; - public final String ISO639_1; - public final String ISO639_2; - - public static final Language AFRIKAANS = Language.create("af", "af", "afr"); - public static final Language ALBANIAN = Language.create("sq", "sq", "alb"); - public static final Language ARABIC = Language.create("ar", "ar", "ara"); - public static final Language BALUCHI = Language.create("bal", "", "bal"); - public static final Language BASQUE = Language.create("eu", "", "baq"); - public static final Language BRAHUI = Language.create("", "", ""); - public static final Language CATALAN = Language.create("ca", "ca", "cat"); - public static final Language CLASSICAL_GREEK = Language.create("grc", "", "grc"); - public static final Language DANISH = Language.create("da", "da", "dan"); - public static final Language DUTCH = Language.create("nl", "ni", "dut"); - public static final Language ENGLISH = Language.create("en", "en", "eng"); - public static final Language ESPERANTO = Language.create("eo", "eo", "epo"); - public static final Language FRENCH = Language.create("fr", "fr", "fre"); - public static final Language FRISIAN = Language.create("fy", "fy", "fri"); - public static final Language GALICIAN = Language.create("gl", "gl", "glg"); - public static final Language GERMAN = Language.create("de", "de", "ger"); - public static final Language GREEK = Language.create("el", "el", "gre"); - public static final Language HAWAIIAN = Language.create("haw", "", "haw"); - public static final Language HEBREW = Language.create("he", "he", "heb"); - public static final Language HUNGARIAN = Language.create("hu", "hu", "hun"); - public static final Language ICELANDIC = Language.create("is", "is", "ice"); - public static final Language IRISH = Language.create("ga", "ga", "gle"); - public static final Language ITALIAN = Language.create("it", "it", "ita"); - public static final Language MALAGASY = Language.create("mg", "mg", "mlg"); - public static final Language MALTESE = Language.create("mt", "mt", "mlt"); - public static final Language NEAPOLITAN_ITALIAN = Language.create("nap", "", "nap"); - public static final Language NORWEGIAN = Language.create("no", "no", "nor"); - public static final Language PORTUGUESE = Language.create("pt", "pt", "por"); - public static final Language PANJABI = Language.create("pa", "pa", "pan"); - public static final Language PERSIAN = Language.create("fa", "fa", "per"); - public static final Language PROVENCAL = Language.create("pro", "", "pro"); - public static final Language PROVENCAL_OCCITAN = Language.create("oc", "oc", "oci"); - public static final Language ROMANIAN = Language.create("ro", "ro", "rum"); - public static final Language SCOTS = Language.create("sco", "", "sco"); - public static final Language SCOTTISH_GAELIC = Language.create("gd", "gd", "gae"); - public static final Language SHETLAND_ENGLISH = Language.create("", "", ""); - public static final Language SPANISH = Language.create("es", "es", "spa"); - public static final Language SWEDISH = Language.create("sv", "sv", "swe"); - public static final Language TAGALOG = Language.create("tl", "tl", "tgl"); - public static final Language TURKISH = Language.create("tr", "tr", "tur"); - public static final Language URDU = Language.create("ur", "ur", "urd"); - public static final Language WALLOON = Language.create("wa", "wa", "wln"); - public static final Language WELSH = Language.create("cy", "cy", "wel"); - public static final Language YIDDISH = Language.create("yi", "yi", "yid"); - - public static Language getLanguage(String lang) { - return LanguageMaps.getLanguage(lang); - } - - public static Language getLanguageForIANA(String iana) { - return LanguageMaps.getLanguageForIANA(iana); - } - - public static Language getLanguageForISO639_2(String iso) { - return LanguageMaps.getLanguageForISO639_2(iso); - } - - private static synchronized Language create(String iana, String iso639_1, String iso639_2) { - Language lang = LanguageMaps.getLanguageForIANA(iana); - - lang = (lang != null ? lang : LanguageMaps.getLanguageForISO639_1(iso639_1)); - lang = (lang != null ? lang : LanguageMaps.getLanguageForISO639_2(iso639_2)); - - return (lang != null ? lang : new Language(iana, iso639_1, iso639_2)); - } - - private static class LanguageMaps { - private static final Map langMapIANA = new HashMap(); - private static final Map langMapISO639_1 = new HashMap(); - private static final Map langMapISO639_2 = new HashMap(); - - static void add(Language l) { - if (l.IANA != null && l.IANA.length() > 0 && !langMapIANA.containsKey(l.IANA)) { - langMapIANA.put(l.IANA, l); - } - - if (l.ISO639_1 != null && l.ISO639_1.length() > 0 && !langMapISO639_1.containsKey(l.ISO639_1)) { - langMapISO639_1.put(l.ISO639_1, l); - } - - if (l.ISO639_2 != null && l.ISO639_2.length() > 0 && !langMapISO639_2.containsKey(l.ISO639_2)) { - langMapISO639_2.put(l.ISO639_2, l); - } - } - - public static Language getLanguage(String lang) { - if (langMapIANA.containsKey(lang)) { - return langMapIANA.get(lang); - } - - return langMapISO639_2.get(lang); - } - - public static Language getLanguageForIANA(String iana) { - return langMapIANA.get(iana); - } - - public static Language getLanguageForISO639_1(String iso) { - return langMapISO639_1.get(iso); - } - - public static Language getLanguageForISO639_2(String iso) { - return langMapISO639_2.get(iso); - } - } - - private Language(String iana, String iso639_1, String iso639_2) { - IANA = iana; - ISO639_1 = iso639_1; - ISO639_2 = iso639_2; - - LanguageMaps.add(this); - } - - private Language() { - IANA = null; - ISO639_1 = null; - ISO639_2 = null; - } -} diff --git a/dspace-api/src/main/java/org/dspace/text/filter/MARC21InitialArticleWord.java b/dspace-api/src/main/java/org/dspace/text/filter/MARC21InitialArticleWord.java deleted file mode 100644 index c82b9ccfcf83..000000000000 --- a/dspace-api/src/main/java/org/dspace/text/filter/MARC21InitialArticleWord.java +++ /dev/null @@ -1,329 +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.text.filter; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.StringUtils; -import org.dspace.services.factory.DSpaceServicesFactory; - -/** - * Implements MARC 21 standards to disregard initial - * definite or indefinite article in sorting. - * - * Note: This only works for languages defined with IANA code entries. - * - * @author Graham Triggs - */ -public class MARC21InitialArticleWord extends InitialArticleWord { - public MARC21InitialArticleWord() { - // Default behaviour is to strip the initial word completely - super(true); - } - - public MARC21InitialArticleWord(boolean stripWord) { - super(stripWord); - } - - /** - * Return the list of definite and indefinite article codes - * for this language. - */ - @Override - protected String[] getArticleWords(String lang) { - // No language - no words - if (StringUtils.isEmpty(lang)) { - return defaultWords; - } - - Language l = Language.getLanguage(lang); - - // Is the language in our map? - if (l != null && ianaArticleMap.containsKey(l.IANA)) { - // Get the list of words for this language - ArticlesForLang articles = ianaArticleMap.get(l.IANA); - - if (articles != null) { - return articles.words; - } - } - - return null; - } - - // Mapping of IANA codes to article word lists - private static Map ianaArticleMap = new HashMap(); - - private static String[] defaultWords = null; - - // Static initialisation - convert word -> languages map - // into language -> words map - static { - /* Define a mapping for article words to the languages that have them. - * Take from: http://www.loc.gov/marc/bibliographic/bdapp-e.html - */ - Object[][] articleWordArray = { - {"a", Language.ENGLISH, Language.GALICIAN, Language.HUNGARIAN, Language.PORTUGUESE, Language.ROMANIAN, - Language.SCOTS, Language.YIDDISH}, - {"a'", Language.SCOTTISH_GAELIC}, - {"al", Language.ROMANIAN}, - {"al-", Language.ARABIC, Language.BALUCHI, Language.BRAHUI, Language.PANJABI, Language.PERSIAN, - Language.TURKISH, Language.URDU}, - {"am", Language.SCOTTISH_GAELIC}, - {"an", Language.ENGLISH, Language.IRISH, Language.SCOTS, Language.SCOTTISH_GAELIC, Language.YIDDISH}, - {"an t-", Language.IRISH, Language.SCOTTISH_GAELIC}, - {"ane", Language.SCOTS}, - {"ang", Language.TAGALOG}, - {"ang mga", Language.TAGALOG}, - {"as", Language.GALICIAN, Language.PORTUGUESE}, - {"az", Language.HUNGARIAN}, - {"bat", Language.BASQUE}, - {"bir", Language.TURKISH}, - {"d'", Language.ENGLISH}, - {"da", Language.SHETLAND_ENGLISH}, - {"das", Language.GERMAN}, - {"de", Language.DANISH, Language.DUTCH, Language.ENGLISH, Language.FRISIAN, Language.NORWEGIAN, - Language.SWEDISH}, - {"dei", Language.NORWEGIAN}, - {"dem", Language.GERMAN}, - {"den", Language.DANISH, Language.GERMAN, Language.NORWEGIAN, Language.SWEDISH}, - {"der", Language.GERMAN, Language.YIDDISH}, - {"des", Language.GERMAN, Language.WALLOON}, - {"det", Language.DANISH, Language.NORWEGIAN, Language.SWEDISH}, - {"di", Language.YIDDISH}, - {"die", Language.AFRIKAANS, Language.GERMAN, Language.YIDDISH}, - {"dos", Language.YIDDISH}, - {"e", Language.NORWEGIAN}, - {"e", Language.FRISIAN}, // should be 'e - leading apostrophes are ignored - {"een", Language.DUTCH}, - {"eene", Language.DUTCH}, - {"egy", Language.HUNGARIAN}, - {"ei", Language.NORWEGIAN}, - {"ein", Language.GERMAN, Language.NORWEGIAN, Language.WALLOON}, - {"eine", Language.GERMAN}, - {"einem", Language.GERMAN}, - {"einen", Language.GERMAN}, - {"einer", Language.GERMAN}, - {"eines", Language.GERMAN}, - {"eit", Language.NORWEGIAN}, - {"el", Language.CATALAN, Language.SPANISH}, - {"el-", Language.ARABIC}, - {"els", Language.CATALAN}, - {"en", Language.CATALAN, Language.DANISH, Language.NORWEGIAN, Language.SWEDISH}, - {"enne", Language.WALLOON}, - {"et", Language.DANISH, Language.NORWEGIAN}, - {"ett", Language.SWEDISH}, - {"eyn", Language.YIDDISH}, - {"eyne", Language.YIDDISH}, - {"gl'", Language.ITALIAN}, - {"gli", Language.PROVENCAL}, - {"ha-", Language.HEBREW}, - {"hai", Language.CLASSICAL_GREEK, Language.GREEK}, - {"he", Language.HAWAIIAN}, - {"h\u0113", Language.CLASSICAL_GREEK, Language.GREEK}, // e macron - {"he-", Language.HEBREW}, - {"heis", Language.GREEK}, - {"hen", Language.GREEK}, - {"hena", Language.GREEK}, - {"henas", Language.GREEK}, - {"het", Language.DUTCH}, - {"hin", Language.ICELANDIC}, - {"hina", Language.ICELANDIC}, - {"hinar", Language.ICELANDIC}, - {"hinir", Language.ICELANDIC}, - {"hinn", Language.ICELANDIC}, - {"hinna", Language.ICELANDIC}, - {"hinnar", Language.ICELANDIC}, - {"hinni", Language.ICELANDIC}, - {"hins", Language.ICELANDIC}, - {"hinu", Language.ICELANDIC}, - {"hinum", Language.ICELANDIC}, - {"hi\u01d2", Language.ICELANDIC}, - {"ho", Language.CLASSICAL_GREEK, Language.GREEK}, - {"hoi", Language.CLASSICAL_GREEK, Language.GREEK}, - {"i", Language.ITALIAN}, - {"ih'", Language.PROVENCAL}, - {"il", Language.ITALIAN, Language.PROVENCAL_OCCITAN}, - {"il-", Language.MALTESE}, - {"in", Language.FRISIAN}, - {"it", Language.FRISIAN}, - {"ka", Language.HAWAIIAN}, - {"ke", Language.HAWAIIAN}, - {"l'", Language.CATALAN, Language.FRENCH, Language.ITALIAN, Language.PROVENCAL_OCCITAN, Language.WALLOON}, - {"l-", Language.MALTESE}, - {"la", Language.CATALAN, Language.ESPERANTO, Language.FRENCH, Language.ITALIAN, Language.PROVENCAL_OCCITAN, - Language.SPANISH}, - {"las", Language.PROVENCAL_OCCITAN, Language.SPANISH}, - {"le", Language.FRENCH, Language.ITALIAN, Language.PROVENCAL_OCCITAN}, - {"les", Language.CATALAN, Language.FRENCH, Language.PROVENCAL_OCCITAN, Language.WALLOON}, - {"lh", Language.PROVENCAL_OCCITAN}, - {"lhi", Language.PROVENCAL_OCCITAN}, - {"li", Language.PROVENCAL_OCCITAN}, - {"lis", Language.PROVENCAL_OCCITAN}, - {"lo", Language.ITALIAN, Language.PROVENCAL_OCCITAN, Language.SPANISH}, - {"los", Language.PROVENCAL_OCCITAN, Language.SPANISH}, - {"lou", Language.PROVENCAL_OCCITAN}, - {"lu", Language.PROVENCAL_OCCITAN}, - {"mga", Language.TAGALOG}, - {"m\u0303ga", Language.TAGALOG}, - {"mia", Language.GREEK}, - {"n", Language.AFRIKAANS, Language.DUTCH, Language.FRISIAN}, // should be 'n - leading - // apostrophes are ignored - {"na", Language.HAWAIIAN, Language.IRISH, Language.SCOTTISH_GAELIC}, - {"na h-", Language.IRISH, Language.SCOTTISH_GAELIC}, - {"nje", Language.ALBANIAN}, - {"ny", Language.MALAGASY}, - {"o", Language.NEAPOLITAN_ITALIAN}, // should be 'o - leading apostrophes are ignored - {"o", Language.GALICIAN, Language.HAWAIIAN, Language.PORTUGUESE, Language.ROMANIAN}, - {"os", Language.PORTUGUESE}, - {"r", Language.ICELANDIC}, // should be 'r - leading apostrophes are ignored - {"s", Language.GERMAN}, // should be 's - leading apostrophes are ignored - {"sa", Language.TAGALOG}, - {"sa mga", Language.TAGALOG}, - {"si", Language.TAGALOG}, - {"sin\u00e1", Language.TAGALOG}, - {"t", Language.DUTCH, Language.FRISIAN}, // should be 't - leading apostrophes are ignored - {"ta", Language.CLASSICAL_GREEK, Language.GREEK}, - {"tais", Language.CLASSICAL_GREEK}, - {"tas", Language.CLASSICAL_GREEK}, - {"t\u0113", Language.CLASSICAL_GREEK}, // e macron - {"t\u0113n", Language.CLASSICAL_GREEK, Language.GREEK}, // e macron - {"t\u0113s", Language.CLASSICAL_GREEK, Language.GREEK}, // e macron - {"the", Language.ENGLISH}, - {"t\u014d", Language.CLASSICAL_GREEK, Language.GREEK}, // o macron - {"tois", Language.CLASSICAL_GREEK}, - {"t\u014dn", Language.CLASSICAL_GREEK, Language.GREEK}, // o macron - {"tou", Language.CLASSICAL_GREEK, Language.GREEK}, - {"um", Language.PORTUGUESE}, - {"uma", Language.PORTUGUESE}, - {"un", Language.CATALAN, Language.FRENCH, Language.ITALIAN, Language.PROVENCAL_OCCITAN, Language.ROMANIAN, - Language.SPANISH}, - {"un'", Language.ITALIAN}, - {"una", Language.CATALAN, Language.ITALIAN, Language.PROVENCAL_OCCITAN, Language.SPANISH}, - {"une", Language.FRENCH}, - {"unei", Language.ROMANIAN}, - {"unha", Language.GALICIAN}, - {"uno", Language.ITALIAN, Language.PROVENCAL_OCCITAN}, - {"uns", Language.PROVENCAL_OCCITAN}, - {"unui", Language.ROMANIAN}, - {"us", Language.PROVENCAL_OCCITAN}, - {"y", Language.WELSH}, - {"ye", Language.ENGLISH}, - {"yr", Language.WELSH} - }; - - // Initialize the lang -> article map - ianaArticleMap = new HashMap(); - - int wordIdx = 0; - int langIdx = 0; - - // Iterate through word/language array - // Generate temporary language map - Map> langWordMap = new HashMap>(); - for (wordIdx = 0; wordIdx < articleWordArray.length; wordIdx++) { - for (langIdx = 1; langIdx < articleWordArray[wordIdx].length; langIdx++) { - Language lang = (Language) articleWordArray[wordIdx][langIdx]; - - if (lang != null && lang.IANA.length() > 0) { - List words = langWordMap.get(lang); - - if (words == null) { - words = new ArrayList(); - langWordMap.put(lang, words); - } - - // Add language to list if we haven't done so already - if (!words.contains(articleWordArray[wordIdx][0])) { - words.add((String) articleWordArray[wordIdx][0]); - } - } - } - } - - // Iterate through languages - for (Map.Entry> langToWord : langWordMap.entrySet()) { - Language lang = langToWord.getKey(); - List wordList = langToWord.getValue(); - - // Convert the list into an array of strings - String[] words = new String[wordList.size()]; - - for (int idx = 0; idx < wordList.size(); idx++) { - words[idx] = wordList.get(idx); - } - - // Sort the array into length order - longest to shortest - // This ensures maximal matching on the article words - Arrays.sort(words, new MARC21InitialArticleWord.InverseLengthComparator()); - - // Add language/article entry to map - ianaArticleMap.put(lang.IANA, new MARC21InitialArticleWord.ArticlesForLang(lang, words)); - } - - // Setup default stop words for null languages - String[] defaultLangs = DSpaceServicesFactory.getInstance().getConfigurationService() - .getArrayProperty("marc21wordfilter.defaultlang"); - if (ArrayUtils.isNotEmpty(defaultLangs)) { - int wordCount = 0; - ArticlesForLang[] afl = new ArticlesForLang[defaultLangs.length]; - - for (int idx = 0; idx < afl.length; idx++) { - Language l = Language.getLanguage(defaultLangs[idx]); - if (l != null && ianaArticleMap.containsKey(l.IANA)) { - afl[idx] = ianaArticleMap.get(l.IANA); - if (afl[idx] != null) { - wordCount += afl[idx].words.length; - } - } - } - - if (wordCount > 0) { - int destPos = 0; - defaultWords = new String[wordCount]; - for (int idx = 0; idx < afl.length; idx++) { - if (afl[idx] != null) { - System.arraycopy(afl[idx].words, 0, defaultWords, destPos, afl[idx].words.length); - destPos += afl[idx].words.length; - } - } - } - } - } - - // Wrapper class for inserting word arrays into a map - private static class ArticlesForLang { - final Language lang; - final String[] words; - - ArticlesForLang(Language lang, String[] words) { - this.lang = lang; - this.words = (String[]) ArrayUtils.clone(words); - } - } - - // Compare strings according to their length - longest to shortest - private static class InverseLengthComparator implements Comparator, Serializable { - @Override - public int compare(Object arg0, Object arg1) { - return ((String) arg1).length() - ((String) arg0).length(); - } - - ; - - } - - ; -} diff --git a/dspace-api/src/main/java/org/dspace/text/filter/StandardInitialArticleWord.java b/dspace-api/src/main/java/org/dspace/text/filter/StandardInitialArticleWord.java deleted file mode 100644 index ade72b150f5c..000000000000 --- a/dspace-api/src/main/java/org/dspace/text/filter/StandardInitialArticleWord.java +++ /dev/null @@ -1,30 +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.text.filter; - -/** - * Implements existing DSpace initial article word behaviour - * - * Note: This only works for languages defined with ISO code entries. - * - * @author Graham Triggs - */ -public class StandardInitialArticleWord extends InitialArticleWord { - private static final String[] articleWords = {"the", "an", "a"}; - - @Override - protected String[] getArticleWords(String lang) { - if (lang != null && lang.startsWith("en")) { - return articleWords; - } - - return null; - } - -} - diff --git a/dspace-api/src/main/java/org/dspace/util/DateMathParser.java b/dspace-api/src/main/java/org/dspace/util/DateMathParser.java index 9ff252e8ce3f..13f9216c9bdb 100644 --- a/dspace-api/src/main/java/org/dspace/util/DateMathParser.java +++ b/dspace-api/src/main/java/org/dspace/util/DateMathParser.java @@ -13,6 +13,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; @@ -107,7 +108,7 @@ public class DateMathParser { private static final Logger LOG = LogManager.getLogger(); - public static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + public static final TimeZone UTC = TimeZone.getTimeZone(ZoneOffset.UTC); /** * Default TimeZone for DateMath rounding (UTC) diff --git a/dspace-api/src/main/java/org/dspace/versioning/VersionHistoryServiceImpl.java b/dspace-api/src/main/java/org/dspace/versioning/VersionHistoryServiceImpl.java index 96c39ac3a8e8..493861df1c60 100644 --- a/dspace-api/src/main/java/org/dspace/versioning/VersionHistoryServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/versioning/VersionHistoryServiceImpl.java @@ -109,9 +109,6 @@ public Version getVersion(Context context, VersionHistory versionHistory, Item i throws SQLException { Version v = versioningService.getVersion(context, item); if (v != null) { - ; - } - { if (versionHistory.equals(v.getVersionHistory())) { return v; } diff --git a/dspace-api/src/main/resources/Messages.properties b/dspace-api/src/main/resources/Messages.properties index efbbeedde053..9d15bd0621a8 100644 --- a/dspace-api/src/main/resources/Messages.properties +++ b/dspace-api/src/main/resources/Messages.properties @@ -72,20 +72,20 @@ org.dspace.checker.ResultsLogger.store-number org.dspace.checker.ResultsLogger.to-be-processed = To be processed org.dspace.checker.ResultsLogger.user-format-description = User format description org.dspace.checker.SimpleReporterImpl.bitstream-id = Bitstream Id -org.dspace.checker.SimpleReporterImpl.bitstream-not-found-report = The following is a BITSTREAM NOT FOUND report for -org.dspace.checker.SimpleReporterImpl.bitstream-will-no-longer-be-processed = The following is a BITSTREAM WILL NO LONGER BE PROCESSED report for +org.dspace.checker.SimpleReporterImpl.bitstream-not-found-report = The following is a BITSTREAM NOT FOUND report from +org.dspace.checker.SimpleReporterImpl.bitstream-will-no-longer-be-processed = The following is a BITSTREAM WILL NO LONGER BE PROCESSED report from org.dspace.checker.SimpleReporterImpl.check-id = Check Id org.dspace.checker.SimpleReporterImpl.checksum = Checksum org.dspace.checker.SimpleReporterImpl.checksum-algorithm = Checksum Algorithm org.dspace.checker.SimpleReporterImpl.checksum-calculated = Checksum Calculated -org.dspace.checker.SimpleReporterImpl.checksum-did-not-match = The following is a CHECKSUM DID NOT MATCH report for +org.dspace.checker.SimpleReporterImpl.checksum-did-not-match = The following is a CHECKSUM DID NOT MATCH report from org.dspace.checker.SimpleReporterImpl.checksum-expected = Checksum Expected org.dspace.checker.SimpleReporterImpl.date-range-to = to org.dspace.checker.SimpleReporterImpl.deleted = Deleted -org.dspace.checker.SimpleReporterImpl.deleted-bitstream-intro = The following is a BITSTREAM SET DELETED report for +org.dspace.checker.SimpleReporterImpl.deleted-bitstream-intro = The following is a BITSTREAM SET DELETED report from org.dspace.checker.SimpleReporterImpl.description = Description org.dspace.checker.SimpleReporterImpl.format-id = Format Id -org.dspace.checker.SimpleReporterImpl.howto-add-unchecked-bitstreams = To add these bitstreams to be checked run the checksum checker with the -u option +org.dspace.checker.SimpleReporterImpl.howto-add-unchecked-bitstreams = To add these bitstreams to be checked run the checksum checker again org.dspace.checker.SimpleReporterImpl.internal-id = Internal Id org.dspace.checker.SimpleReporterImpl.name = Name org.dspace.checker.SimpleReporterImpl.no-bitstreams-changed = There were no bitstreams found with changed checksums diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2026.03.25__entity_types_caching.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2026.03.25__entity_types_caching.sql new file mode 100644 index 000000000000..1b6db9f52847 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2026.03.25__entity_types_caching.sql @@ -0,0 +1,11 @@ +-- +-- 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/ +-- + +-- H2 does not support upper and hence the migration differs slightly from the postgres version +-- In a test environment this will not make a meaningful impact +CREATE INDEX entity_type_label_upper_idx ON entity_type (label); diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.10.29__Fix-request-items-with-deleted-bitstreams.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.10.29__Fix-request-items-with-deleted-bitstreams.sql new file mode 100644 index 000000000000..4f0c54c975c6 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.10.29__Fix-request-items-with-deleted-bitstreams.sql @@ -0,0 +1,14 @@ +-- +-- 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/ +-- + +DELETE +FROM requestitem +WHERE bitstream_id IN + (SELECT bs.uuid + FROM bitstream AS bs + WHERE bs.deleted IS TRUE) diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2026.03.25__entity_types_caching.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2026.03.25__entity_types_caching.sql new file mode 100644 index 000000000000..2a1510ff6f04 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2026.03.25__entity_types_caching.sql @@ -0,0 +1,9 @@ +-- +-- 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/ +-- + +CREATE INDEX entity_type_label_upper_idx ON entity_type (UPPER(label)); diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.4_2026.03.28__Fix-bundle2bitstream-with-deleted-bitstreams.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.4_2026.03.28__Fix-bundle2bitstream-with-deleted-bitstreams.sql new file mode 100644 index 000000000000..fa37ac35e81a --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.4_2026.03.28__Fix-bundle2bitstream-with-deleted-bitstreams.sql @@ -0,0 +1,17 @@ +-- +-- 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/ +-- + +-- Remove orphaned bundle2bitstream rows that reference deleted bitstreams. +-- These orphaned rows prevent 'dspace cleanup' from expunging deleted +-- bitstreams due to FK constraint violations. +DELETE +FROM bundle2bitstream +WHERE bitstream_id IN + (SELECT bs.uuid + FROM bitstream AS bs + WHERE bs.deleted IS TRUE) 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 e693d26e538e..5cfef3bd22fb 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 @@ -70,6 +70,7 @@ + diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/bitstore.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/bitstore.xml index 15bb3ef1580b..84fdc8282204 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/bitstore.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/bitstore.xml @@ -27,6 +27,9 @@ + + + @@ -34,6 +37,32 @@ + + + + + + + + + + + + + + + + + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/core-services.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/core-services.xml index c3825da174be..f951d3e5fb7b 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/core-services.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/core-services.xml @@ -32,7 +32,7 @@ - + @@ -163,10 +163,11 @@ - + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml index 0d074362279e..a7c725c524fe 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml @@ -23,7 +23,6 @@ - @@ -46,7 +45,6 @@ - @@ -66,21 +64,14 @@ - - - - - - - + - - + diff --git a/dspace-api/src/test/java/org/dspace/AbstractDSpaceIntegrationTest.java b/dspace-api/src/test/java/org/dspace/AbstractDSpaceIntegrationTest.java index 791fdbc66abc..2822cdcf6018 100644 --- a/dspace-api/src/test/java/org/dspace/AbstractDSpaceIntegrationTest.java +++ b/dspace-api/src/test/java/org/dspace/AbstractDSpaceIntegrationTest.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.net.URL; import java.sql.SQLException; +import java.time.ZoneOffset; import java.util.Properties; import java.util.TimeZone; @@ -73,8 +74,10 @@ public static void initTestEnvironment() { //Stops System.exit(0) throws exception instead of exitting System.setSecurityManager(new NoExitSecurityManager()); - //set a standard time zone for the tests - TimeZone.setDefault(TimeZone.getTimeZone("Europe/Dublin")); + // All tests should assume UTC timezone by default (unless overridden in the test itself) + // This ensures that Spring doesn't attempt to change the timezone of dates that are read from the + // database (via Hibernate). We store all dates in the database as UTC. + TimeZone.setDefault(TimeZone.getTimeZone(ZoneOffset.UTC)); //load the properties of the tests testProps = new Properties(); diff --git a/dspace-api/src/test/java/org/dspace/AbstractDSpaceTest.java b/dspace-api/src/test/java/org/dspace/AbstractDSpaceTest.java index 136af83f076f..4452955a3b64 100644 --- a/dspace-api/src/test/java/org/dspace/AbstractDSpaceTest.java +++ b/dspace-api/src/test/java/org/dspace/AbstractDSpaceTest.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.net.URL; import java.sql.SQLException; +import java.time.ZoneOffset; import java.util.Properties; import java.util.TimeZone; @@ -82,8 +83,10 @@ protected AbstractDSpaceTest() { } @BeforeClass public static void initKernel() { try { - //set a standard time zone for the tests - TimeZone.setDefault(TimeZone.getTimeZone("Europe/Dublin")); + // All tests should assume UTC timezone by default (unless overridden in the test itself) + // This ensures that Spring doesn't attempt to change the timezone of dates that are read from the + // database (via Hibernate). We store all dates in the database as UTC. + TimeZone.setDefault(TimeZone.getTimeZone(ZoneOffset.UTC)); //load the properties of the tests testProps = new Properties(); diff --git a/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java b/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java index 76b3fe131be0..8c03e7f1a204 100644 --- a/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java +++ b/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java @@ -179,15 +179,40 @@ public void setUp() throws Exception { */ @After public void destroy() throws Exception { - // Cleanup our global context object + // Contain the blast radius of teardown failures: the shared static state (Solr cores, authority + // cache, configuration, builder cache) must be reset regardless of whether builder cleanup threw. + // Otherwise a single flake in one test's teardown poisons every subsequent test in the class. + Exception primaryFailure = null; try { AbstractBuilder.cleanupObjects(); parentCommunity = null; cleanupContext(); } catch (Exception e) { - throw new RuntimeException("Error cleaning up builder objects & context object", e); + primaryFailure = new RuntimeException("Error cleaning up builder objects & context object", e); + } finally { + try { + resetSharedState(); + } catch (Exception e) { + if (primaryFailure == null) { + primaryFailure = e; + } else { + primaryFailure.addSuppressed(e); + } + } } + if (primaryFailure != null) { + throw primaryFailure; + } + } + /** + * Reset all shared static state between tests: Solr cores, authority cache, QA events, configuration + * service, and the builder cache. Called from {@link #destroy()} inside a finally block so this always + * runs, even if earlier teardown steps failed. + * + * @throws Exception if reloading configuration or resetting the builder cache fails + */ + private void resetSharedState() throws Exception { ServiceManager serviceManager = DSpaceServicesFactory.getInstance().getServiceManager(); // Clear the search core. diff --git a/dspace-api/src/test/java/org/dspace/app/bulkedit/DSpaceCSVTest.java b/dspace-api/src/test/java/org/dspace/app/bulkedit/DSpaceCSVIT.java similarity index 65% rename from dspace-api/src/test/java/org/dspace/app/bulkedit/DSpaceCSVTest.java rename to dspace-api/src/test/java/org/dspace/app/bulkedit/DSpaceCSVIT.java index 21a1a67dde2b..f4e1e7f2892f 100644 --- a/dspace-api/src/test/java/org/dspace/app/bulkedit/DSpaceCSVTest.java +++ b/dspace-api/src/test/java/org/dspace/app/bulkedit/DSpaceCSVIT.java @@ -9,6 +9,9 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; import java.io.BufferedWriter; @@ -20,7 +23,15 @@ import org.apache.commons.io.FileUtils; import org.apache.logging.log4j.Logger; -import org.dspace.AbstractUnitTest; +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.junit.Before; import org.junit.Test; @@ -29,11 +40,39 @@ * * @author Stuart Lewis */ -public class DSpaceCSVTest extends AbstractUnitTest { +public class DSpaceCSVIT extends AbstractIntegrationTestWithDatabase { /** * log4j category */ - private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(DSpaceCSVTest.class); + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(DSpaceCSVIT.class); + + ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + + Item testItem; + + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + configurationService.addPropertyValue("metadata.hide.dc.subject", true); + + Collection parentCollection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Parent Collection") + .build(); + + testItem = ItemBuilder.createItem(context, parentCollection).withTitle("Test Item") + .withMetadata("dc", "description", "provenance", "provenance") + .withMetadata("dc", "subject", null, "hidden subject") + .build(); + + context.restoreAuthSystemState(); + } /** * Test the reading and parsing of CSV files @@ -147,4 +186,63 @@ public void testDSpaceCSV() { fail("IO Error while creating test CSV file"); } } + + /** + * Test the hidden metadata for csv is respected + * + */ + @Test + public void testHiddenDspaceCSV() throws Exception { + + DSpaceCSV dSpaceCSV = new DSpaceCSV(false); + + dSpaceCSV.addItem(testItem); + + List lines = dSpaceCSV.getCSVLines(); + + assertThat(lines.size(), equalTo(1)); + + DSpaceCSVLine line = lines.get(0); + + List subject = line.get("dc.subject"); + List provenance = line.get("dc.description.provenance"); + List title = line.get("dc.title"); + + assertNull(subject); + assertNull(provenance); + assertNotNull(title); + assertEquals("Test Item", title.get(0)); + + } + + /** + * Test the hidden metadata is still shown when force is applied. + * + */ + @Test + public void testHiddenDspaceForceCSV() throws Exception { + + DSpaceCSV dSpaceCSV = new DSpaceCSV(true); + + dSpaceCSV.addItem(testItem); + + List lines = dSpaceCSV.getCSVLines(); + + assertThat(lines.size(), equalTo(1)); + + DSpaceCSVLine line = lines.get(0); + + List subject = line.get("dc.subject"); + List provenance = line.get("dc.description.provenance"); + List title = line.get("dc.title"); + + assertNotNull(subject); + assertNotNull(provenance); + assertEquals("hidden subject", subject.get(0)); + assertEquals("provenance", provenance.get(0)); + assertNotNull(title); + assertEquals("Test Item", title.get(0)); + + } + } 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 63a87a48f554..15a6371e920b 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 @@ -251,4 +251,35 @@ public void exportMetadataSearchNonExistinFacetsTest() throws Exception { assertNotNull(exception); assertEquals("nonExisting is not a valid search filter", exception.getMessage()); } + + @Test + public void exportMetadataSearchDoubleQuotedArgumentTest() throws Exception { + context.turnOffAuthorisationSystem(); + Item quotedItem1 = ItemBuilder.createItem(context, collection) + .withTitle("The Special Runnable Item") + .withSubject("quoted-subject") + .build(); + Item quotedItem2 = ItemBuilder.createItem(context, collection) + .withTitle("The Special Item") + .withSubject("quoted-subject") + .build(); + context.restoreAuthSystemState(); + + int result = runDSpaceScript( + "metadata-export-search", + "-q", "title:\"Special Runnable\"", + "-n", filename); + + assertEquals(0, result); + + Item[] expectedResult = new Item[] {quotedItem1}; + checkItemsPresentInFile(filename, expectedResult); + + File file = new File(filename); + try (Reader reader = Files.newReader(file, Charset.defaultCharset()); + CSVReader csvReader = new CSVReader(reader)) { + List lines = csvReader.readAll(); + assertEquals("Unexpected extra items in export", 2, lines.size()); + } + } } 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 de1dcc91c9a1..9f28834eff32 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 @@ -10,6 +10,7 @@ import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertTrue; import static junit.framework.TestCase.fail; +import static org.junit.Assert.assertNotNull; import java.io.BufferedWriter; import java.io.File; @@ -43,6 +44,8 @@ import org.dspace.scripts.configuration.ScriptConfiguration; import org.dspace.scripts.factory.ScriptServiceFactory; import org.dspace.scripts.service.ScriptService; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; import org.junit.Before; import org.junit.Test; @@ -54,6 +57,8 @@ public class MetadataImportIT extends AbstractIntegrationTestWithDatabase { = EPersonServiceFactory.getInstance().getEPersonService(); private final RelationshipService relationshipService = ContentServiceFactory.getInstance().getRelationshipService(); + private final ConfigurationService configurationService + = DSpaceServicesFactory.getInstance().getConfigurationService(); private Collection collection; private Collection publicationCollection; @@ -305,4 +310,71 @@ public void performImportScript(String[] csv, boolean useTemplate) throws Except csvFile.delete(); } } -} + + @Test + public void metadataImportExceedsLimitTest() throws Exception { + configurationService.setProperty("bulkedit.import.max.items", 1); + String[] csv = {"id,collection,dc.title", + "+," + collection.getHandle() + ",\"Title 1\"", + "+," + collection.getHandle() + ",\"Title 2\""}; + File csvFile = File.createTempFile("dspace-test-import", "csv"); + try { + try (BufferedWriter out = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(csvFile), "UTF-8"))) { + for (String csvLine : csv) { + out.write(csvLine + "\n"); + } + } + String fileLocation = csvFile.getAbsolutePath(); + String[] args = new String[] {"metadata-import", "-f", fileLocation, "-e", eperson.getEmail(), "-s"}; + TestDSpaceRunnableHandler testDSpaceRunnableHandler = new TestDSpaceRunnableHandler(); + ScriptLauncher.handleScript( + args, ScriptLauncher.getConfig(kernelImpl), testDSpaceRunnableHandler, kernelImpl); + + assertNotNull("The handler should contain an exception", + testDSpaceRunnableHandler.getException()); + + assertTrue("The exception cause should be a MetadataImportException", + testDSpaceRunnableHandler.getException().getCause() instanceof MetadataImportException); + + String exceptionMessage = testDSpaceRunnableHandler.getException().getCause().getMessage(); + assertTrue("The error message does not contain the expected text.", + exceptionMessage.contains("exceeds the configured maximum of 1")); + } finally { + csvFile.delete(); + } + } + + @Test + public void metadataImportWithItemCountBelowLimitTest() throws Exception { + configurationService.setProperty("bulkedit.import.max.items", 2); + String[] csv = {"id,collection,dc.title", + "+," + collection.getHandle() + ",\"Title 1\"", + "+," + collection.getHandle() + ",\"Title 2\""}; + performImportScript(csv); + Item importedItem1 = findItemByName("Title 1"); + Item importedItem2 = findItemByName("Title 2"); + assertNotNull("Should have imported Title 1", importedItem1); + assertNotNull("Should have imported Title 2", importedItem2); + } + + @Test + public void metadataImportWithLimitDisabledTest() throws Exception { + configurationService.setProperty("bulkedit.import.max.items", 0); + String[] csv = {"id,collection,dc.title", + "+," + collection.getHandle() + ",\"Title 1\"", + "+," + collection.getHandle() + ",\"Title 2\""}; + performImportScript(csv); + Item importedItem1 = findItemByName("Title 1"); + Item importedItem2 = findItemByName("Title 2"); + assertNotNull("Should have imported Title 1 with limit disabled", importedItem1); + assertNotNull("Should have imported Title 2 with limit disabled", importedItem2); + } + + @Test + public void metadataImportWithEmptyCSVTest() throws Exception { + String[] csv = {"id,collection,dc.title"}; + performImportScript(csv); + assertEquals(0, IteratorUtils.toList(itemService.findAll(context)).size()); + } +} \ No newline at end of file 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 index b518f19ff4d3..ca91bb5dc9ce 100644 --- a/dspace-api/src/test/java/org/dspace/app/client/DSpaceHttpClientFactoryTest.java +++ b/dspace-api/src/test/java/org/dspace/app/client/DSpaceHttpClientFactoryTest.java @@ -17,6 +17,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import java.net.InetAddress; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -107,7 +108,14 @@ public void testBuildWithProxyConfiguredAndHostToIgnoreSet() throws Exception { @Test public void testBuildWithProxyConfiguredAndHostPrefixToIgnoreSet() throws Exception { - setHttpProxyOnConfigurationService("local*", "www.test.com"); + // Get hostname assigned to 127.0.0.1 (usually is "localhost", but not always) + InetAddress address = InetAddress.getByAddress(new byte[]{127, 0, 0, 1}); + String hostname = address.getHostName(); + // Take first 4 characters hostname as the prefix (e.g. "loca" in "localhost") + String hostnamePrefix = hostname.substring(0, 4); + // Save hostname prefix to our list of hosts to ignore, followed by an asterisk. + // (This should result in our Proxy ignoring our localhost) + setHttpProxyOnConfigurationService(hostnamePrefix + "*", "www.test.com"); CloseableHttpClient httpClient = httpClientFactory.build(); assertThat(mockProxy.getRequestCount(), is(0)); assertThat(mockServer.getRequestCount(), is(0)); @@ -122,7 +130,14 @@ public void testBuildWithProxyConfiguredAndHostPrefixToIgnoreSet() throws Except @Test public void testBuildWithProxyConfiguredAndHostSuffixToIgnoreSet() throws Exception { - setHttpProxyOnConfigurationService("www.test.com", "*host"); + // Get hostname assigned to 127.0.0.1 (usually is "localhost", but not always) + InetAddress address = InetAddress.getByAddress(new byte[]{127, 0, 0, 1}); + String hostname = address.getHostName(); + // Take last 4 characters hostname as the suffix (e.g. "host" in "localhost") + String hostnameSuffix = hostname.substring(hostname.length() - 4); + // Save hostname suffix to our list of hosts to ignore, preceded by an asterisk. + // (This should result in our Proxy ignoring our localhost) + setHttpProxyOnConfigurationService("www.test.com", "*" + hostnameSuffix); CloseableHttpClient httpClient = httpClientFactory.build(); assertThat(mockProxy.getRequestCount(), is(0)); assertThat(mockServer.getRequestCount(), is(0)); diff --git a/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java b/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java index 73f97b2a6a7c..bec0de59619c 100644 --- a/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java +++ b/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java @@ -10,11 +10,16 @@ import static org.dspace.app.ldn.action.LDNActionStatus.ABORT; import static org.dspace.app.ldn.action.LDNActionStatus.CONTINUE; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.io.IOException; import java.io.InputStream; +import java.nio.file.Path; import java.sql.SQLException; import java.util.List; @@ -40,6 +45,7 @@ import org.dspace.content.Collection; import org.dspace.content.Item; import org.dspace.content.WorkspaceItem; +import org.dspace.core.LDN; import org.dspace.eperson.EPerson; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; @@ -232,6 +238,32 @@ public void testLDNMessageConsumerRequestReviewWithInvalidLdnUrl() throws Except response.close(); } + @Test + public void testLDNLegalPath() throws Exception { + try { + // Path traversal will be calculated properly + // but this still ends up at a legal base + LDN message = LDN.getLDNMessage("../../config/ldn/request-review"); + assertNotNull(message); + } catch (IOException e) { + fail("IOException should NOT have been thrown for legal template path: " + e.getMessage()); + } + } + + @Test + public void testLDNIllegalPath() throws Exception { + try { + String badAbsolutePath = Path.of(configurationService.getProperty("dspace.dir")) + .resolve("config/dspace.cfg") + .toAbsolutePath() + .toString(); + LDN.getLDNMessage(badAbsolutePath); + fail("IOException should have been thrown for illegal template path"); + } catch (IOException e) { + assertTrue(e.getMessage().contains("Illegal file path attempted for I/O (ldn):")); + } + } + @Override @After public void destroy() throws Exception { diff --git a/dspace-api/src/test/java/org/dspace/app/mediafilter/JPEGFilterTest.java b/dspace-api/src/test/java/org/dspace/app/mediafilter/JPEGFilterTest.java new file mode 100644 index 000000000000..1181dc7a60f0 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/mediafilter/JPEGFilterTest.java @@ -0,0 +1,270 @@ +/** + * 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.mediafilter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import org.dspace.AbstractUnitTest; +import org.dspace.content.Item; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.junit.Test; +import org.mockito.Mock; + +public class JPEGFilterTest extends AbstractUnitTest { + + @Mock + private ConfigurationService mockConfigurationService; + + @Mock + private DSpaceServicesFactory mockDSpaceServicesFactory; + + @Mock + private InputStream mockInputStream; + + @Mock + private Item mockItem; + + /** + * Tests that the convertRotationToDegrees method returns 0 for an input value + * that doesn't match any of the defined rotation cases. + */ + @Test + public void testConvertRotationToDegrees_UnknownValue_ReturnsZero() { + int result = JPEGFilter.convertRotationToDegrees(5); + assertEquals(0, result); + } + + /** + * Test getNormalizedInstance method with a null input. + * This tests the edge case of passing a null BufferedImage to the method. + * The method should throw a NullPointerException when given a null input. + */ + @Test(expected = NullPointerException.class) + public void testGetNormalizedInstanceWithNullInput() { + JPEGFilter filter = new JPEGFilter(); + filter.getNormalizedInstance(null); + } + + /** + * Test getThumbDim method with a null BufferedImage input. + * This tests the edge case where the input image is null, which should result in an exception. + */ + @Test(expected = NullPointerException.class) + public void testGetThumbDimWithNullBufferedImage() throws Exception { + JPEGFilter filter = new JPEGFilter(); + Item currentItem = null; + BufferedImage buf = null; + boolean verbose = false; + int xmax = 100; + int ymax = 100; + boolean blurring = false; + boolean hqscaling = false; + int brandHeight = 0; + int brandFontPoint = 0; + int rotation = 0; + String brandFont = null; + + filter.getThumbDim( + currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, + brandHeight, brandFontPoint, rotation, brandFont + ); + } + + /** + * Tests that the rotateImage method returns the original image when the rotation angle is 0. + * This is an edge case explicitly handled in the method implementation. + */ + @Test + public void testRotateImageWithZeroAngle() { + BufferedImage originalImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); + BufferedImage rotatedImage = JPEGFilter.rotateImage(originalImage, 0); + assertSame( + "When rotation angle is 0, the original image should be returned", + originalImage, rotatedImage + ); + } + + /** + * Test case for convertRotationToDegrees method when input is 6. + * Expected to return 90 degrees for the rotation value of 6. + */ + @Test + public void test_convertRotationToDegrees_whenInputIs6_returns90() { + int input = 6; + int expected = 90; + int result = JPEGFilter.convertRotationToDegrees(input); + assertEquals(expected, result); + } + + /** + * Tests that getBlurredInstance method applies a blur effect to the input image. + * It verifies that the returned image is not null, has the same dimensions as the input, + * and is different from the original image (indicating that blurring has occurred). + */ + @Test + public void test_getBlurredInstance_appliesBlurEffect() { + JPEGFilter filter = new JPEGFilter(); + BufferedImage original = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); + + BufferedImage blurred = filter.getBlurredInstance(original); + + assertNotNull("Blurred image should not be null", blurred); + assertEquals("Width should be the same", original.getWidth(), blurred.getWidth()); + assertEquals("Height should be the same", original.getHeight(), blurred.getHeight()); + assertNotEquals("Blurred image should be different from original", original, blurred); + } + + /** + * Test case for getBundleName method of JPEGFilter class. + * This test verifies that the getBundleName method returns the expected string "THUMBNAIL". + */ + @Test + public void test_getBundleName_returnsExpectedString() { + JPEGFilter filter = new JPEGFilter(); + String result = filter.getBundleName(); + assertEquals("THUMBNAIL", result); + } + + /** + * Tests that the getDescription method returns the expected string "Generated Thumbnail". + * This verifies that the method correctly provides the description for the JPEG filter. + */ + @Test + public void test_getDescription_1() { + JPEGFilter filter = new JPEGFilter(); + String description = filter.getDescription(); + assertEquals("Generated Thumbnail", description); + } + + /** + * Tests that getFilteredName method appends ".jpg" to the input filename. + */ + @Test + public void test_getFilteredName_appendsJpgExtension() { + JPEGFilter filter = new JPEGFilter(); + String oldFilename = "testimage"; + String expectedResult = "testimage.jpg"; + String actualResult = filter.getFilteredName(oldFilename); + assertEquals(expectedResult, actualResult); + } + + /** + * Test case for getFormatString method of JPEGFilter class. + * Verifies that the method returns the expected string "JPEG". + */ + @Test + public void test_getFormatString_returnsJPEG() { + JPEGFilter filter = new JPEGFilter(); + String result = filter.getFormatString(); + assertEquals("JPEG", result); + } + + /** + * Tests the behavior of getImageRotationUsingImageReader when an ImageProcessingException occurs. + * This test verifies that the method handles an ImageProcessingException by logging the error + * and returning 0 degrees rotation. + */ + @Test + public void test_getImageRotationUsingImageReader_imageProcessingException() { + InputStream errorStream = new InputStream() { + @Override + public int read() throws IOException { + throw new IOException("Simulated image processing error"); + } + }; + int result = JPEGFilter.getImageRotationUsingImageReader(errorStream); + assertEquals(0, result); + } + + /** + * Testcase for getImageRotationUsingImageReader when the image doesn't contain orientation metadata. + * This test verifies that the method returns 0 when there's no ExifIFD0Directory + * or when it doesn't contain the TAG_ORIENTATION. + */ + @Test + public void test_getImageRotationUsingImageReader_noOrientationMetadata() throws IOException { + URL resource = this.getClass().getResource("cat.jpg"); + int rotationAngle = -1; + try (InputStream inputStream = new FileInputStream(resource.getFile())) { + // Call the method under test + rotationAngle = JPEGFilter.getImageRotationUsingImageReader(inputStream); + } + assertEquals(0, rotationAngle); + } + + /** + * Tests the getImageRotationUsingImageReader method when the image contains + * valid EXIF orientation metadata. + * + * This test verifies that the method correctly reads the orientation tag + * from the EXIF metadata and returns the appropriate rotation angle in degrees. + */ + @Test + public void test_getImageRotationUsingImageReader_withValidExifOrientation() throws Exception { + // Create a mock InputStream with EXIF metadata containing orientation information + URL resource = this.getClass().getResource("cat-rotated-90.jpg"); + int rotationAngle = -1; + try (InputStream inputStream = new FileInputStream(resource.getFile())) { + // Call the method under test + rotationAngle = JPEGFilter.getImageRotationUsingImageReader(inputStream); + } + + // Assert the expected rotation angle + // Note: The expected value should be adjusted based on the mock data + assertEquals(90, rotationAngle); + } + + /** + * Tests the getScaledInstance method of JPEGFilter class with higher quality scaling. + * This test verifies that the method correctly scales down an image in multiple passes + * when higherQuality is true and the image dimensions are larger than the target dimensions. + */ + @Test + public void test_getScaledInstance() { + JPEGFilter filter = new JPEGFilter(); + BufferedImage originalImage = new BufferedImage(400, 300, BufferedImage.TYPE_INT_RGB); + int targetWidth = 100; + int targetHeight = 75; + Object hint = RenderingHints.VALUE_INTERPOLATION_BILINEAR; + boolean higherQuality = true; + + BufferedImage result = filter.getScaledInstance(originalImage, targetWidth, targetHeight, hint, higherQuality); + + assertNotNull(result); + assertEquals(targetWidth, result.getWidth()); + assertEquals(targetHeight, result.getHeight()); + } + + /** + * Tests the rotateImage method with a non-zero angle. + * This test verifies that the image is rotated correctly when given a non-zero angle. + */ + @Test + public void test_rotateImage_nonZeroAngle() { + BufferedImage originalImage = new BufferedImage(100, 50, BufferedImage.TYPE_INT_RGB); + int angle = 90; + + BufferedImage rotatedImage = JPEGFilter.rotateImage(originalImage, angle); + + assertNotNull(rotatedImage); + assertEquals(50, rotatedImage.getWidth()); + assertEquals(100, rotatedImage.getHeight()); + } + +} diff --git a/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java b/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java new file mode 100644 index 000000000000..e0f0fc57a9ea --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java @@ -0,0 +1,136 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.authenticate; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import jakarta.servlet.http.HttpServletRequest; +import org.dspace.AbstractUnitTest; +import org.dspace.content.MetadataField; +import org.dspace.content.service.MetadataFieldService; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.service.EPersonService; +import org.dspace.services.ConfigurationService; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +/** + * Unit tests for ShibAuthentication + */ +public class ShibAuthenticationTest extends AbstractUnitTest { + + private ShibAuthentication shibAuthentication; + private EPersonService ePersonService; + private ConfigurationService configurationService; + + @Before + public void setup() { + shibAuthentication = new ShibAuthentication(); + ePersonService = mock(EPersonService.class); + shibAuthentication.ePersonService = ePersonService; + configurationService = mock(ConfigurationService.class); + shibAuthentication.configurationService = configurationService; + when(configurationService.getProperty("authentication-shibboleth.netid-header")).thenReturn("SHIB-NETID"); + when(configurationService.getProperty("authentication-shibboleth.email-header")).thenReturn("SHIB-MAIL"); + when(configurationService.getArrayProperty("authentication-shibboleth.eperson.metadata")) + .thenReturn(new String[]{"SHIB-telephone => eperson.phone"}); + when(configurationService.getBooleanProperty("authentication-shibboleth.eperson.metadata.autocreate", true)) + .thenReturn(true); + MetadataFieldService metadataFieldService = mock(MetadataFieldService.class); + shibAuthentication.metadataFieldService = metadataFieldService; + + try { + when(metadataFieldService.findByElement(any(Context.class), any(String.class), any(String.class), any())) + .thenReturn(mock(MetadataField.class)); + } catch (Exception e) { + // ignore checked exceptions from mock + } + } + + @Test + public void testPhoneMetadataUpdateOrder() throws Exception { + Context context = mock(Context.class); + HttpServletRequest request = mock(HttpServletRequest.class); + EPerson eperson = mock(EPerson.class); + when(request.getAttribute("SHIB-NETID")).thenReturn("test-user"); + when(request.getAttribute("SHIB-MAIL")).thenReturn("test@example.com"); + String phoneValue = "555-1234"; + when(request.getAttribute("SHIB-telephone")).thenReturn(phoneValue); + shibAuthentication.initialize(context); + assertNotNull("metadataHeaderMap should be initialized", shibAuthentication.metadataHeaderMap); + assertTrue("metadataHeaderMap should contain SHIB-telephone", shibAuthentication.metadataHeaderMap + .containsKey("SHIB-telephone")); + shibAuthentication.updateEPerson(context, request, eperson); + ArgumentCaptor languageCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor valueCaptor = ArgumentCaptor.forClass(String.class); + + verify(ePersonService, times(1)).setMetadataSingleValue( + any(Context.class), + eq(eperson), + eq("eperson"), + eq("phone"), + isNull(), + languageCaptor.capture(), + valueCaptor.capture() + ); + + assertNull("The language argument should be NULL.", languageCaptor.getValue()); + assertEquals("The value argument should be the phone number.", phoneValue, valueCaptor.getValue()); + } + + @Test + public void testInitializeLoadsMultipleMappings() throws Exception { + Context context = mock(Context.class); + when(configurationService.getArrayProperty("authentication-shibboleth.eperson.metadata")) + .thenReturn(new String[]{ + "SHIB-telephone => eperson.phone", + "SHIB-dept => eperson.department" + }); + shibAuthentication.initialize(context); + + assertNotNull("metadataHeaderMap should be initialized", shibAuthentication.metadataHeaderMap); + assertTrue("metadataHeaderMap should contain SHIB-telephone", shibAuthentication.metadataHeaderMap + .containsKey("SHIB-telephone")); + assertTrue("metadataHeaderMap should contain SHIB-dept", shibAuthentication.metadataHeaderMap + .containsKey("SHIB-dept")); + } + + @Test + public void testNoMetadataMappingNoUpdate() throws Exception { + Context context = mock(Context.class); + HttpServletRequest request = mock(HttpServletRequest.class); + EPerson eperson = mock(EPerson.class); + when(configurationService.getArrayProperty("authentication-shibboleth.eperson.metadata")) + .thenReturn(new String[0]); + shibAuthentication.initialize(context); + shibAuthentication.updateEPerson(context, request, eperson); + + verify(ePersonService, times(0)).setMetadataSingleValue( + any(Context.class), + any(EPerson.class), + anyString(), + anyString(), + any(), + any(), + any() + ); + } +} 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 511df79f1e50..b6be6d1f3ac2 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 @@ -11,6 +11,7 @@ import java.io.InputStream; +import org.dspace.external.OrcidConnectionException; import org.dspace.external.OrcidRestConnector; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; @@ -38,7 +39,7 @@ public void init() { * 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() { + public void setupNoResultsSearch() throws OrcidConnectionException { when(orcidRestConnector.get(ArgumentMatchers.startsWith("search?"), ArgumentMatchers.any())) .thenAnswer(new Answer() { @Override @@ -51,7 +52,7 @@ public InputStream answer(InvocationOnMock invocation) { * 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() { + public void setupSingleSearch() throws OrcidConnectionException { when(orcidRestConnector.get(ArgumentMatchers.startsWith("search?q=Bollini"), ArgumentMatchers.any())) .thenAnswer(new Answer() { @Override @@ -64,7 +65,7 @@ public InputStream answer(InvocationOnMock invocation) { * 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() { + public void setupSearchWithResults() throws OrcidConnectionException { when(orcidRestConnector.get(ArgumentMatchers.endsWith("/person"), ArgumentMatchers.any())) .thenAnswer(new Answer() { @Override diff --git a/dspace-api/src/test/java/org/dspace/authorize/AuthorizeServiceTest.java b/dspace-api/src/test/java/org/dspace/authorize/AuthorizeServiceTest.java index 70eaa2a0b909..e8bb428db53a 100644 --- a/dspace-api/src/test/java/org/dspace/authorize/AuthorizeServiceTest.java +++ b/dspace-api/src/test/java/org/dspace/authorize/AuthorizeServiceTest.java @@ -9,14 +9,24 @@ package org.dspace.authorize; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; import org.dspace.AbstractUnitTest; import org.dspace.authorize.factory.AuthorizeServiceFactory; import org.dspace.authorize.service.ResourcePolicyService; +import org.dspace.content.Bundle; +import org.dspace.content.Collection; import org.dspace.content.Community; +import org.dspace.content.Item; +import org.dspace.content.WorkspaceItem; import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.BundleService; import org.dspace.content.service.CollectionService; import org.dspace.content.service.CommunityService; +import org.dspace.content.service.InstallItemService; +import org.dspace.content.service.ItemService; +import org.dspace.content.service.WorkspaceItemService; import org.dspace.core.Constants; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; @@ -38,6 +48,10 @@ public class AuthorizeServiceTest extends AbstractUnitTest { .getResourcePolicyService(); protected CommunityService communityService = ContentServiceFactory.getInstance().getCommunityService(); protected CollectionService collectionService = ContentServiceFactory.getInstance().getCollectionService(); + protected ItemService itemService = ContentServiceFactory.getInstance().getItemService(); + protected BundleService bundleService = ContentServiceFactory.getInstance().getBundleService(); + protected WorkspaceItemService workspaceItemService = ContentServiceFactory.getInstance().getWorkspaceItemService(); + protected InstallItemService installItemService = ContentServiceFactory.getInstance().getInstallItemService(); public AuthorizeServiceTest() { } @@ -127,6 +141,89 @@ public void testauthorizeMethodRespectSpecialGroups() { throw new AssertionError(ex); } } + + /** + * When a bundle is created it should inherit custom policies (deduped) + * from the item, as otherwise bitstream bundles created via filter-media etc. + * will be created without READ policies + */ + @Test + public void testInheritanceOfCustomPolicies() { + try { + context.turnOffAuthorisationSystem(); + Community community = communityService.create(null, context); + Collection collection = collectionService.create(context, community); + WorkspaceItem wsItem = workspaceItemService.create(context, collection, false); + Item item = installItemService.installItem(context, wsItem); + // Simulate access conditions adding READ policy to the item + ResourcePolicy itemCustomRead = resourcePolicyService.create(context, eperson, null); + itemCustomRead.setAction(Constants.READ); + itemCustomRead.setRpType(ResourcePolicy.TYPE_CUSTOM); + // Simulate a random ADMIN action policy that might have been added manually + ResourcePolicy itemCustomAdmin = resourcePolicyService.create(context, eperson, null); + itemCustomAdmin.setAction(Constants.ADMIN); + itemCustomAdmin.setRpType(ResourcePolicy.TYPE_CUSTOM); + List customPolicies = new ArrayList<>(); + customPolicies.add(itemCustomRead); + customPolicies.add(itemCustomAdmin); + authorizeService.addPolicies(context, customPolicies, item); + // Create a bundle, this should call inheritPolicies via itemService.addBundle + Bundle bundle = bundleService.create(context, item, "THUMBNAIL"); + List newPolicies = authorizeService + .findPoliciesByDSOAndType(context, bundle, ResourcePolicy.TYPE_CUSTOM); + Assert.assertEquals("Bundle should inherit custom policy from item", 1, newPolicies.size()); + Assert.assertNotEquals("Bundle should ONLY inherit non-admin custom policy from item", + Constants.ADMIN, newPolicies.get(0).getAction()); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + context.restoreAuthSystemState(); + } + } + + /** + * For other DSOs (which pass false) and for a bundle explicitly calling + * inheritPolicies(..., false), the TYPE_CUSTOM policies should not be inherited + * but other non-admin policies should be inherited as usual + */ + @Test + public void testNonInheritanceOfCustomPolicies() { + try { + context.turnOffAuthorisationSystem(); + Community community = communityService.create(null, context); + Collection collection = collectionService.create(context, community); + WorkspaceItem wsItem = workspaceItemService.create(context, collection, false); + Item item = installItemService.installItem(context, wsItem); + Bundle bundle = bundleService.create(context, item, "THUMBNAIL"); + // Simulate a custom READ policy added by access conditions step + ResourcePolicy itemCustomRead = resourcePolicyService.create(context, eperson, null); + itemCustomRead.setAction(Constants.READ); + itemCustomRead.setRpType(ResourcePolicy.TYPE_CUSTOM); + // Simulate an ordinary default read item policy inherited from collection + ResourcePolicy itemDefaultRead = resourcePolicyService.create(context, eperson, null); + itemDefaultRead.setAction(Constants.READ); + itemDefaultRead.setRpType(ResourcePolicy.TYPE_INHERITED); + List customPolicies = new ArrayList<>(); + customPolicies.add(itemCustomRead); + customPolicies.add(itemDefaultRead); + authorizeService.addPolicies(context, customPolicies, item); + // Now, inherit policies for bundle with includeCustom=false (which is how other DSOs behave) + authorizeService.inheritPolicies(context, item, bundle, false); + List newCustomPolicies = authorizeService + .findPoliciesByDSOAndType(context, bundle, ResourcePolicy.TYPE_CUSTOM); + List newInheritedPolicies = authorizeService + .findPoliciesByDSOAndType(context, bundle, ResourcePolicy.TYPE_INHERITED); + Assert.assertEquals("Bundle should not inherit custom policy from item, if false passed", + 0, newCustomPolicies.size()); + Assert.assertEquals("Bundle should inherit non-custom, non-admin policies as usual", + ResourcePolicy.TYPE_INHERITED, newInheritedPolicies.get(0).getRpType()); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + context.restoreAuthSystemState(); + } + } + // // @Test // public void testIsCollectionAdmin() throws SQLException, AuthorizeException, IOException { diff --git a/dspace-api/src/test/java/org/dspace/authorize/RegexPasswordValidatorIT.java b/dspace-api/src/test/java/org/dspace/authorize/RegexPasswordValidatorTest.java similarity index 92% rename from dspace-api/src/test/java/org/dspace/authorize/RegexPasswordValidatorIT.java rename to dspace-api/src/test/java/org/dspace/authorize/RegexPasswordValidatorTest.java index 7286fb8e8374..7a6a1ca4a10f 100644 --- a/dspace-api/src/test/java/org/dspace/authorize/RegexPasswordValidatorIT.java +++ b/dspace-api/src/test/java/org/dspace/authorize/RegexPasswordValidatorTest.java @@ -11,22 +11,19 @@ import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.when; -import org.dspace.AbstractIntegrationTest; +import org.dspace.AbstractUnitTest; 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 RegexPasswordValidator}. * * @author Luca Giamminonni (luca.giamminonni at 4science.it) */ -@RunWith(MockitoJUnitRunner.class) -public class RegexPasswordValidatorIT extends AbstractIntegrationTest { +public class RegexPasswordValidatorTest extends AbstractUnitTest { @Mock private ConfigurationService configurationService; diff --git a/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java b/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java index 5e9545fcafbd..d3e4d0ff8f78 100644 --- a/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java @@ -113,6 +113,14 @@ public ItemBuilder withScopusIdentifier(String scopus) { return addMetadataValue(item, "dc", "identifier", "scopus", scopus); } + public ItemBuilder withISSN(String issn) { + return addMetadataValue(item, "dc", "identifier", "issn", issn); + } + + public ItemBuilder withISBN(String isbn) { + return addMetadataValue(item, "dc", "identifier", "isbn", isbn); + } + public ItemBuilder withRelationFunding(String funding) { return addMetadataValue(item, "dc", "relation", "funding", funding); } diff --git a/dspace-api/src/test/java/org/dspace/builder/util/AbstractBuilderCleanupUtil.java b/dspace-api/src/test/java/org/dspace/builder/util/AbstractBuilderCleanupUtil.java index 6a8daa432eb6..fd5c504a717f 100644 --- a/dspace-api/src/test/java/org/dspace/builder/util/AbstractBuilderCleanupUtil.java +++ b/dspace-api/src/test/java/org/dspace/builder/util/AbstractBuilderCleanupUtil.java @@ -12,6 +12,8 @@ import java.util.List; import java.util.Map; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.builder.AbstractBuilder; import org.dspace.builder.BitstreamBuilder; import org.dspace.builder.BitstreamFormatBuilder; @@ -45,6 +47,8 @@ */ public class AbstractBuilderCleanupUtil { + private static final Logger log = LogManager.getLogger(AbstractBuilderCleanupUtil.class); + private final LinkedHashMap> map = new LinkedHashMap<>(); @@ -98,15 +102,34 @@ public void addToMap(AbstractBuilder abstractBuilder) { /** * This method takes care of iterating over all the AbstractBuilders in the predefined order and calls * the cleanup method to delete the objects from the database. - * @throws Exception If something goes wrong + *

+ * Each builder is cleaned up inside its own try/catch so that a single failure (e.g. the intermittent + * Hibernate {@link java.util.ConcurrentModificationException} in resource registry cleanup) does not + * abort cleanup of the remaining builders. The first failure is rethrown at the end with any subsequent + * failures attached as suppressed exceptions, so nothing is silently swallowed. + *

+ * @throws Exception If one or more builders fail to clean up */ public void cleanupBuilders() throws Exception { + Exception firstFailure = null; for (Map.Entry> entry : map.entrySet()) { List list = entry.getValue(); for (AbstractBuilder abstractBuilder : list) { - abstractBuilder.cleanup(); + try { + abstractBuilder.cleanup(); + } catch (Exception e) { + log.error("Error cleaning up builder {}", abstractBuilder.getClass().getName(), e); + if (firstFailure == null) { + firstFailure = e; + } else { + firstFailure.addSuppressed(e); + } + } } } + if (firstFailure != null) { + throw firstFailure; + } } /** diff --git a/dspace-api/src/test/java/org/dspace/checker/ChecksumCheckerIT.java b/dspace-api/src/test/java/org/dspace/checker/ChecksumCheckerIT.java index 173ffc79c476..34198ff1ebfe 100644 --- a/dspace-api/src/test/java/org/dspace/checker/ChecksumCheckerIT.java +++ b/dspace-api/src/test/java/org/dspace/checker/ChecksumCheckerIT.java @@ -35,15 +35,6 @@ import org.junit.Before; import org.junit.Test; -/** - * UMD Customization - * - * A modified version of this class was provided to DSpace in - * Pull Request 10508. - * - * This class should be replaced with the DSpace version, once this application - * has been upgraded to a DSpace version containing the pull request. - */ public class ChecksumCheckerIT extends AbstractIntegrationTestWithDatabase { protected List bitstreams; protected MostRecentChecksumService checksumService = @@ -83,12 +74,12 @@ public void setup() throws Exception { // a random order. To verify that the expected bitstreams were // processed, reset the timestamps so that the bitstreams are // checked in a specific order (oldest first). - Date checksumInstant = Date.from(Instant.ofEpochMilli(0)); + Instant checksumInstant = Instant.ofEpochMilli(0); for (Bitstream bitstream: bitstreams) { MostRecentChecksum mrc = checksumService.findByBitstream(context, bitstream); - mrc.setProcessStartDate(checksumInstant); - mrc.setProcessEndDate(checksumInstant); - checksumInstant = new Date(checksumInstant.getTime() + 10000); + mrc.setProcessStartDate(Date.from(checksumInstant)); + mrc.setProcessEndDate(Date.from(checksumInstant)); + checksumInstant = checksumInstant.plusSeconds(10); } context.commit(); } diff --git a/dspace-api/src/test/java/org/dspace/content/BitstreamExpungeIT.java b/dspace-api/src/test/java/org/dspace/content/BitstreamExpungeIT.java new file mode 100644 index 000000000000..284ecabacc68 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/content/BitstreamExpungeIT.java @@ -0,0 +1,247 @@ +/** + * 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.content; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.InputStream; +import java.util.Iterator; +import java.util.UUID; + +import org.apache.commons.io.IOUtils; +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.app.requestitem.RequestItem; +import org.dspace.app.requestitem.factory.RequestItemServiceFactory; +import org.dspace.app.requestitem.service.RequestItemService; +import org.dspace.authorize.factory.AuthorizeServiceFactory; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.builder.BitstreamBuilder; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.builder.RequestItemBuilder; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.BitstreamService; +import org.dspace.content.service.BundleService; +import org.junit.Before; +import org.junit.Test; + +/** + * Integration tests for {@link BitstreamServiceImpl#expunge}. + * + * Verifies that expunge() defensively cleans up bundle2bitstream, requestitem, + * and resourcepolicy references before the hard-delete, so historical orphaned + * rows do not cause FK constraint violations. + * + * @author Bram Luyten (bram at atmire.com) + */ +public class BitstreamExpungeIT extends AbstractIntegrationTestWithDatabase { + + private final BitstreamService bitstreamService = + ContentServiceFactory.getInstance().getBitstreamService(); + private final BundleService bundleService = + ContentServiceFactory.getInstance().getBundleService(); + private final RequestItemService requestItemService = + RequestItemServiceFactory.getInstance().getRequestItemService(); + private final AuthorizeService authorizeService = + AuthorizeServiceFactory.getInstance().getAuthorizeService(); + + private Collection collection; + + @Before + public void setup() { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).build(); + collection = CollectionBuilder.createCollection(context, parentCommunity).build(); + context.restoreAuthSystemState(); + } + + /** + * Smoke test the normal path: a bitstream that has been soft-deleted via + * delete() can be expunged. delete() already cleans up FK references, so + * expunge() should succeed without any defensive work. + */ + @Test + public void testExpungeAfterNormalDelete() throws Exception { + context.turnOffAuthorisationSystem(); + + Item item = ItemBuilder.createItem(context, collection).withTitle("Test item").build(); + Bitstream bitstream = BitstreamBuilder + .createBitstream(context, item, toInputStream("test content")) + .build(); + UUID bitstreamId = bitstream.getID(); + + bitstreamService.delete(context, bitstream); + context.commit(); + + bitstream = bitstreamService.find(context, bitstreamId); + assertTrue("Bitstream should be marked as deleted", bitstream.isDeleted()); + + bitstreamService.expunge(context, bitstream); + context.commit(); + + assertNull("Bitstream should not exist after expunge", + bitstreamService.find(context, bitstreamId)); + + context.restoreAuthSystemState(); + } + + /** + * Simulate the historical bug scenario: a bitstream is marked deleted but + * its bundle2bitstream row was never cleaned up. expunge() must remove the + * leftover bundle association so the hard-delete does not hit a FK + * constraint violation. + */ + @Test + public void testExpungeOrphanedBundleReference() throws Exception { + context.turnOffAuthorisationSystem(); + + Item item = ItemBuilder.createItem(context, collection).withTitle("Test item").build(); + Bitstream bitstream = BitstreamBuilder + .createBitstream(context, item, toInputStream("test content")) + .build(); + UUID bitstreamId = bitstream.getID(); + UUID bundleId = bitstream.getBundles().get(0).getID(); + + // Mark the bitstream as deleted WITHOUT going through delete(), so the + // bundle relationship stays in place. This is the orphan state the PR + // is designed to recover from. + bitstream.setDeleted(true); + bitstreamService.update(context, bitstream); + context.commit(); + + bitstream = bitstreamService.find(context, bitstreamId); + assertEquals("Bundle association should still exist before expunge", + 1, bitstream.getBundles().size()); + + bitstreamService.expunge(context, bitstream); + context.commit(); + + assertNull("Bitstream should be removed after expunge", + bitstreamService.find(context, bitstreamId)); + + Bundle bundle = bundleService.find(context, bundleId); + assertFalse("Bundle should no longer reference the bitstream", + bundle.getBitstreams().stream().anyMatch(b -> b.getID().equals(bitstreamId))); + + context.restoreAuthSystemState(); + } + + /** + * Variant where the orphaned bitstream is also the bundle's primary + * bitstream. expunge() must unset the primary bitstream pointer before + * hard-deleting, otherwise the bundle_primary_bitstream_id_fkey constraint + * fires. + */ + @Test + public void testExpungeOrphanedPrimaryBitstream() throws Exception { + context.turnOffAuthorisationSystem(); + + Item item = ItemBuilder.createItem(context, collection).withTitle("Test item").build(); + Bitstream bitstream = BitstreamBuilder + .createBitstream(context, item, toInputStream("test content")) + .build(); + UUID bitstreamId = bitstream.getID(); + Bundle bundle = bitstream.getBundles().get(0); + UUID bundleId = bundle.getID(); + + bundle.setPrimaryBitstreamID(bitstream); + bundleService.update(context, bundle); + + bitstream.setDeleted(true); + bitstreamService.update(context, bitstream); + context.commit(); + + // Re-fetch in a fresh session state, simulating the cleanup script + // path which iterates deleted bitstreams in a separate session. + bitstream = bitstreamService.find(context, bitstreamId); + bitstreamService.expunge(context, bitstream); + context.commit(); + + assertNull("Bitstream should be removed after expunge", + bitstreamService.find(context, bitstreamId)); + assertNull("Bundle should no longer have a primary bitstream", + bundleService.find(context, bundleId).getPrimaryBitstream()); + + context.restoreAuthSystemState(); + } + + /** + * Variant where a RequestItem references the orphaned bitstream. expunge() + * must remove the request item so the requestitem_bitstream_id_fkey + * constraint does not fire on the hard-delete. + */ + @Test + public void testExpungeOrphanedRequestItem() throws Exception { + context.turnOffAuthorisationSystem(); + + Item item = ItemBuilder.createItem(context, collection).withTitle("Test item").build(); + Bitstream bitstream = BitstreamBuilder + .createBitstream(context, item, toInputStream("test content")) + .build(); + UUID bitstreamId = bitstream.getID(); + + RequestItemBuilder.createRequestItem(context, item, bitstream).build(); + + bitstream.setDeleted(true); + bitstreamService.update(context, bitstream); + context.commit(); + + Iterator before = requestItemService.findByBitstreamId(context, bitstreamId); + assertTrue("RequestItem should exist before expunge", before.hasNext()); + + bitstream = bitstreamService.find(context, bitstreamId); + bitstreamService.expunge(context, bitstream); + context.commit(); + + assertNull("Bitstream should be removed after expunge", + bitstreamService.find(context, bitstreamId)); + assertFalse("RequestItem should be removed after expunge", + requestItemService.findByBitstreamId(context, bitstreamId).hasNext()); + + context.restoreAuthSystemState(); + } + + /** + * Verify that policies attached to the bitstream are removed by expunge(), + * so resource policy rows do not survive the hard-delete. + */ + @Test + public void testExpungeRemovesPolicies() throws Exception { + context.turnOffAuthorisationSystem(); + + Item item = ItemBuilder.createItem(context, collection).withTitle("Test item").build(); + Bitstream bitstream = BitstreamBuilder + .createBitstream(context, item, toInputStream("test content")) + .build(); + UUID bitstreamId = bitstream.getID(); + + assertFalse("Bitstream should have inherited policies", + authorizeService.getPolicies(context, bitstream).isEmpty()); + + bitstream.setDeleted(true); + bitstreamService.update(context, bitstream); + context.commit(); + + bitstream = bitstreamService.find(context, bitstreamId); + bitstreamService.expunge(context, bitstream); + context.commit(); + + assertNull("Bitstream should be removed after expunge", + bitstreamService.find(context, bitstreamId)); + + context.restoreAuthSystemState(); + } + + private InputStream toInputStream(String content) { + return IOUtils.toInputStream(content, "UTF-8"); + } +} diff --git a/dspace-api/src/test/java/org/dspace/content/BitstreamTest.java b/dspace-api/src/test/java/org/dspace/content/BitstreamTest.java index e85a0fc7b78d..abf19f328210 100644 --- a/dspace-api/src/test/java/org/dspace/content/BitstreamTest.java +++ b/dspace-api/src/test/java/org/dspace/content/BitstreamTest.java @@ -12,6 +12,7 @@ import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -25,6 +26,9 @@ import java.io.FileInputStream; import java.io.IOException; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; import java.util.List; import java.util.UUID; @@ -148,6 +152,44 @@ public void testFindAll() throws SQLException { assertTrue("testFindAll 2", added); } + @Test + public void testFindAllBatches() throws Exception { + //Adding some data for processing and cleaning this up at the end + context.turnOffAuthorisationSystem(); + File f = new File(testProps.get("test.bitstream").toString()); + List inserted = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + Bitstream bs = bitstreamService.create(context, new FileInputStream(f)); + inserted.add(bs); + } + context.restoreAuthSystemState(); + + // sorted list of all bitstreams + List all = bitstreamService.findAll(context); + List expected = new ArrayList<>(all); + expected.sort(Comparator.comparing(bs -> bs.getID().toString())); + + int total = bitstreamService.countTotal(context); + int batchSize = 2; + int numberOfBatches = (int) Math.ceil((double) total / batchSize); + + //collect in batches + List collected = new ArrayList<>(); + for (int i = 0; i < numberOfBatches; i++) { + Iterator it = bitstreamService.findAll(context, batchSize, i * batchSize); + it.forEachRemaining(collected::add); + } + + assertEquals("Batched results should match sorted findAll", expected, collected); + + // Cleanup + context.turnOffAuthorisationSystem(); + for (Bitstream b : inserted) { + bitstreamService.delete(context, b); + } + context.restoreAuthSystemState(); + } + /** * Test of create method, of class Bitstream. */ diff --git a/dspace-api/src/test/java/org/dspace/content/CollectionTest.java b/dspace-api/src/test/java/org/dspace/content/CollectionTest.java index a177571ffa46..d943a4a31613 100644 --- a/dspace-api/src/test/java/org/dspace/content/CollectionTest.java +++ b/dspace-api/src/test/java/org/dspace/content/CollectionTest.java @@ -1159,6 +1159,184 @@ public void testFindAuthorizedOptimized() throws Exception { assertFalse("testFindAuthorizeOptimized D.C", personDCollections.contains(collectionC)); } + /** + * Test of findAuthorizedEpersonAndGroups method, of class Collection. + * We create some collections and a user and groups and subgroups and add the user to one subgroup + * and one collection + * The parent group will be added to the other collection. + */ + @Test + public void testFindAuthorizedByEPerson() throws Exception { + context.turnOffAuthorisationSystem(); + Community com = communityService.create(null, context); + Collection collectionA = collectionService.create(context, com); + Collection collectionB = collectionService.create(context, com); + Collection collectionC = collectionService.create(context, com); + + com.addCollection(collectionA); + com.addCollection(collectionB); + com.addCollection(collectionC); + + Group groupParent = groupService.create(context); + Group groupChild = groupService.create(context); + + groupService.addMember(context, groupParent, groupChild); + + EPerson epersonA = ePersonService.create(context); + + //Add epersonA to the child group + groupService.addMember(context, groupChild, epersonA); + + //personA can submit to collectionA and collectionC + authorizeService.addPolicy(context, collectionA, Constants.ADD, epersonA); + authorizeService.addPolicy(context, collectionB, Constants.ADD, groupParent); + + context.restoreAuthSystemState(); + + context.setCurrentUser(epersonA); + List personACollections = + collectionService.findAuthorized(context, null, List.of(Constants.ADD, Constants.ADMIN)); + assertTrue("testFindAuthorizedByEPerson A", personACollections.size() == 2); + assertTrue("testFindAuthorizedByEPerson A.A", personACollections.contains(collectionA)); + assertTrue("testFindAuthorizedByEPerson A.B", personACollections.contains(collectionB)); + assertFalse("testFindAuthorizedByEPerson A.C", personACollections.contains(collectionC)); + } + + /** + * Test of testFindAuthorizedEPersonCommunityAdmin method, of class Collection. + * This will test what collections care retrieved if a user is a Com Administrator + * eperson A is Top of B (and by the caso of B,C and D) but not of E + * eperson E is Top of E nad of D so it can get THE E and D Collections + * + */ + @Test + public void testFindAuthorizedEPersonCommunityAdmin() throws Exception { + context.turnOffAuthorisationSystem(); + Community comA = communityService.create(null, context); + Community comB = communityService.create(null, context); + Community comC = communityService.create(null, context); + Community comD = communityService.create(null, context); + Community comE = communityService.create(null, context); + + Collection collectionA1 = collectionService.create(context, comA); + Collection collectionC1 = collectionService.create(context, comC); + Collection collectionC2 = collectionService.create(context, comC); + Collection collectionD1 = collectionService.create(context, comD); + Collection collectionE1 = collectionService.create(context, comE); + Collection collectionE2 = collectionService.create(context, comE); + + //Create Com hierarchies + comA.addSubCommunity(comB); + comA.addSubCommunity(comC); + comB.addSubCommunity(comD); + + comA.addCollection(collectionA1); + comC.addCollection(collectionC1); + comC.addCollection(collectionC2); + comD.addCollection(collectionD1); + comE.addCollection(collectionE1); + comE.addCollection(collectionE2); + + + Group groupA = groupService.create(context); + Group groupB = groupService.create(context); + Group groupC = groupService.create(context); + Group groupD = groupService.create(context); + Group groupE = groupService.create(context); + + EPerson epersonA = ePersonService.create(context); + EPerson epersonB = ePersonService.create(context); + + //Add epersonA to the child group + groupService.addMember(context, groupA, epersonA); + //Add epersonB to the child group + groupService.addMember(context, groupE, epersonB); + groupService.addMember(context, groupD, epersonB); + + //personA can submit to collectionA and collectionB + authorizeService.addPolicy(context, comA, Constants.ADMIN, groupA); + authorizeService.addPolicy(context, comD, Constants.ADMIN, groupD); + authorizeService.addPolicy(context, comE, Constants.ADMIN, groupE); + + context.restoreAuthSystemState(); + + //PersonA Can get AllCollection From Top to Bottom com ComA, but not from ComE + context.setCurrentUser(epersonA); + List personACollectionsAdminCommA = + collectionService.findAuthorized(context, null, List.of(Constants.ADD, Constants.ADMIN)); + assertTrue("testFindAuthorizedEPersonCommunityAdmin A", personACollectionsAdminCommA.size() == 4); + assertTrue("testFindAuthorizedEPersonCommunityAdmin A.A", personACollectionsAdminCommA + .containsAll(List.of(collectionA1, collectionD1, collectionC1, collectionC2))); + assertFalse("testFindAuthorizedEPersonCommunityAdmin A.B", personACollectionsAdminCommA + .containsAll(List.of(collectionE1, collectionE2))); + + //PersonB Can get AllCollection From Top to Bottom com ComE, but not from ComA + context.setCurrentUser(epersonB); + List personACollectionsAdminCommE = + collectionService.findAuthorized(context, null, List.of(Constants.ADD, Constants.ADMIN)); + assertTrue("testFindAuthorizedEPersonCommunityAdmin B", personACollectionsAdminCommE.size() == 3); + assertFalse("testFindAuthorizedEPersonCommunityAdmin B.A", personACollectionsAdminCommE + .containsAll(List.of(collectionA1, collectionC1, collectionC2))); + assertTrue("testFindAuthorizedEPersonCommunityAdmin B.B", personACollectionsAdminCommE + .containsAll(List.of(collectionD1, collectionE1, collectionE2))); + } + + /** + * Test of testFindNotAuthorizedEPersonDifferentActions method, of class Collection. + * We create some collections and a user and a group add the user as ADMIN by adding ti + * toa group and that group to a Collection and add the user as submitter to another + * we pass actions that shouldn't return collections if only those actions are passed + * And we test if only on collection is retrieved if we pass the Corresponding action + */ + @Test + public void testFindAuthorizedEPersonDifferentActions() throws Exception { + context.turnOffAuthorisationSystem(); + Community com = communityService.create(null, context); + Collection collectionA = collectionService.create(context, com); + Collection collectionB = collectionService.create(context, com); + + com.addCollection(collectionA); + com.addCollection(collectionB); + + Group group = groupService.create(context); + + EPerson epersonA = ePersonService.create(context); + + //Add epersonA to the child group + groupService.addMember(context, group, epersonA); + + //personA can submit to collectionA and collectionB + authorizeService.addPolicy(context, collectionA, Constants.ADD, epersonA); + authorizeService.addPolicy(context, collectionB, Constants.ADMIN, group); + + context.restoreAuthSystemState(); + + //Person does not Have other permission than ADD - So should not return a Colelction if we pass other + //Actions. In this case WRITE OR DELETE + context.setCurrentUser(epersonA); + List personACollectionsRD = + collectionService.findAuthorized(context, null, List.of(Constants.WRITE, Constants.DELETE)); + assertTrue("testFindAuthorizedEPersonDifferentActions A", personACollectionsRD.isEmpty()); + assertFalse("testFindAuthorizedEPersonDifferentActions A.A", personACollectionsRD.contains(collectionA)); + assertFalse("testFindAuthorizedEPersonDifferentActions A.B", personACollectionsRD.contains(collectionB)); + + //But It Should get Collection B if we pass the ADMIN Action too + List personACollectionsADD = + collectionService.findAuthorized(context, null, + List.of(Constants.WRITE, Constants.DELETE, Constants.ADD)); + assertTrue("testFindAuthorizedEPersonDifferentActions B", personACollectionsADD.size() == 1); + assertTrue("testFindAuthorizedEPersonDifferentActions B.A", personACollectionsADD.contains(collectionA)); + assertFalse("testFindAuthorizedEPersonDifferentActions B.B", personACollectionsADD.contains(collectionB)); + + //But It Should get Collection A if we pass the ADD Action too + List personACollections = + collectionService.findAuthorized(context, null, + List.of(Constants.WRITE, Constants.DELETE, Constants.ADMIN)); + assertTrue("testFindAuthorizedEPersonDifferentActions C", personACollections.size() == 1); + assertFalse("testFindAuthorizedEPersonDifferentActions C.A", personACollections.contains(collectionA)); + assertTrue("testFindAuthorizedEPersonDifferentActions C.B", personACollections.contains(collectionB)); + } + /** * Test of countItems method, of class Collection. */ diff --git a/dspace-api/src/test/java/org/dspace/content/DuplicateDetectionTest.java b/dspace-api/src/test/java/org/dspace/content/DuplicateDetectionIT.java similarity index 97% rename from dspace-api/src/test/java/org/dspace/content/DuplicateDetectionTest.java rename to dspace-api/src/test/java/org/dspace/content/DuplicateDetectionIT.java index 0b6c909f03e8..424233b47898 100644 --- a/dspace-api/src/test/java/org/dspace/content/DuplicateDetectionTest.java +++ b/dspace-api/src/test/java/org/dspace/content/DuplicateDetectionIT.java @@ -17,8 +17,6 @@ import java.util.List; import java.util.Optional; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.dspace.AbstractIntegrationTestWithDatabase; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; @@ -31,7 +29,6 @@ import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; -import org.junit.Before; import org.junit.Test; /** @@ -40,7 +37,7 @@ * * @author Kim Shepherd */ -public class DuplicateDetectionTest extends AbstractIntegrationTestWithDatabase { +public class DuplicateDetectionIT extends AbstractIntegrationTestWithDatabase { private DuplicateDetectionService duplicateDetectionService = ContentServiceFactory.getInstance() .getDuplicateDetectionService(); private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); @@ -54,9 +51,7 @@ public class DuplicateDetectionTest extends AbstractIntegrationTestWithDatabase private final String item1Title = "Public item I"; private final String item1Author = "Smith, Donald"; - private static final Logger log = LogManager.getLogger(); - - @Before + @Override public void setUp() throws Exception { super.setUp(); // Temporarily enable duplicate detection and set comparison distance to 1 @@ -100,7 +95,7 @@ public void setUp() throws Exception { .withAuthor("Smith, Donald Y.") .withSubject("ExtraEntry 3") .build(); - + context.restoreAuthSystemState(); } @@ -210,7 +205,7 @@ public void testSearchDuplicates() throws Exception { public void testSearchDuplicatesWithReservedSolrCharacters() throws Exception { - + context.turnOffAuthorisationSystem(); Item item4 = ItemBuilder.createItem(context, col) .withTitle("Testing: An Important Development Step") .withIssueDate(item1IssueDate) @@ -223,6 +218,7 @@ public void testSearchDuplicatesWithReservedSolrCharacters() throws Exception { .withAuthor("Smith, Donald X.") .withSubject("ExtraEntry 2") .build(); + context.restoreAuthSystemState(); // Get potential duplicates of item 4 and make sure no exceptions are thrown List potentialDuplicates = new ArrayList<>(); @@ -254,6 +250,7 @@ public void testSearchDuplicatesWithReservedSolrCharacters() throws Exception { @Test public void testSearchDuplicatesWithVeryLongTitle() throws Exception { + context.turnOffAuthorisationSystem(); Item item6 = ItemBuilder.createItem(context, col) .withTitle("Testing: This title is over 200 characters long and should behave just the same as a " + "shorter title, with or without reserved characters. This integration test will prove that " + @@ -272,6 +269,8 @@ public void testSearchDuplicatesWithVeryLongTitle() throws Exception { .withSubject("ExtraEntry 2") .build(); + context.restoreAuthSystemState(); + // Get potential duplicates of item 4 and make sure no exceptions are thrown List potentialDuplicates = new ArrayList<>(); try { @@ -303,6 +302,8 @@ public void testSearchDuplicatesExactMatch() throws Exception { // Set distance to 0 manually configurationService.setProperty("duplicate.comparison.distance", 0); + context.turnOffAuthorisationSystem(); + Item item8 = ItemBuilder.createItem(context, col) .withTitle("This integration test will prove that the edit distance of 0 results in an exact match") .withIssueDate(item1IssueDate) @@ -323,7 +324,7 @@ public void testSearchDuplicatesExactMatch() throws Exception { .withAuthor("Smith, Donald X.") .withSubject("ExtraEntry") .build(); - + context.restoreAuthSystemState(); // Get potential duplicates of item 4 and make sure no exceptions are thrown List potentialDuplicates = new ArrayList<>(); try { @@ -387,6 +388,8 @@ public void testSearchDuplicatesWithMultipleFields() throws Exception { configurationService.setProperty("duplicate.comparison.metadata.field", new String[]{"dc.title", "dc.contributor.author"}); + context.turnOffAuthorisationSystem(); + Item item10 = ItemBuilder.createItem(context, col) .withTitle("Compare both title and author") .withIssueDate(item1IssueDate) @@ -407,6 +410,8 @@ public void testSearchDuplicatesWithMultipleFields() throws Exception { .withSubject("ExtraEntry 2") .build(); + context.restoreAuthSystemState(); + // Get potential duplicates of item 10 and make sure no exceptions are thrown List potentialDuplicates = new ArrayList<>(); try { 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 00dbf2994d98..1f520926b982 100644 --- a/dspace-api/src/test/java/org/dspace/content/ItemTest.java +++ b/dspace-api/src/test/java/org/dspace/content/ItemTest.java @@ -1604,6 +1604,27 @@ public void testMoveSameCollection() throws Exception { verify(itemServiceSpy, times(0)).delete(context, it); } + /** + * Test of move with inherit default policies method, of class Item, where both Collections are the same. + */ + @Test + public void testMoveSameCollectionWithInheritDefaultPolicies() throws Exception { + context.turnOffAuthorisationSystem(); + while (it.getCollections().size() > 1) { + it.removeCollection(it.getCollections().get(0)); + } + + Collection collection = it.getCollections().get(0); + it.setOwningCollection(collection); + ItemService itemServiceSpy = spy(itemService); + + itemService.move(context, it, collection, collection, true); + context.restoreAuthSystemState(); + assertThat("testMoveSameCollection 0", it.getOwningCollection(), notNullValue()); + assertThat("testMoveSameCollection 1", it.getOwningCollection(), equalTo(collection)); + verify(itemServiceSpy, times(0)).delete(context, it); + } + /** * Test of hasUploadedFiles method, of class Item. */ diff --git a/dspace-api/src/test/java/org/dspace/content/VersioningTest.java b/dspace-api/src/test/java/org/dspace/content/VersioningTest.java index c23fe7831967..a0a8b82ec150 100644 --- a/dspace-api/src/test/java/org/dspace/content/VersioningTest.java +++ b/dspace-api/src/test/java/org/dspace/content/VersioningTest.java @@ -170,4 +170,23 @@ public void testVersionDelete() throws Exception { assertThat("Test_version_handle_delete", handleService.resolveToObject(context, handle), nullValue()); context.restoreAuthSystemState(); } + + @Test + public void testGetVersionWithNullPointerException() throws Exception { + context.turnOffAuthorisationSystem(); + // Create item without version + Community community = communityService.create(null, context); + Collection col = collectionService.create(context, community); + WorkspaceItem is = workspaceItemService.create(context, col, false); + Item itemWithoutVersion = installItemService.installItem(context, is); + VersionHistory versionHistory = versionHistoryService.findByItem(context, originalItem); + try { + Version result = versionHistoryService.getVersion(context, versionHistory, itemWithoutVersion); + assertThat("getVersion should return null for item without version", result, nullValue()); + } catch (NullPointerException npe) { + fail("NullPointerException should not be thrown. Method should return null: " + npe.getMessage()); + } finally { + context.restoreAuthSystemState(); + } + } } diff --git a/dspace-api/src/test/java/org/dspace/content/authority/DSpaceControlledVocabularyTest.java b/dspace-api/src/test/java/org/dspace/content/authority/DSpaceControlledVocabularyTest.java index 255b070e5eac..524c6407b7bd 100644 --- a/dspace-api/src/test/java/org/dspace/content/authority/DSpaceControlledVocabularyTest.java +++ b/dspace-api/src/test/java/org/dspace/content/authority/DSpaceControlledVocabularyTest.java @@ -8,6 +8,7 @@ package org.dspace.content.authority; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import java.io.IOException; @@ -86,6 +87,7 @@ public void testGetMatches() throws IOException, ClassNotFoundException { CoreServiceFactory.getInstance().getPluginService().getNamedPlugin(Class.forName(PLUGIN_INTERFACE), "farm"); assertNotNull(instance); Choices result = instance.getMatches(text, start, limit, locale); + assertNotEquals("At least one match expected", 0, result.values.length); assertEquals("north 40", result.values[0].value); } diff --git a/dspace-api/src/test/java/org/dspace/content/logic/LogicalFilterTest.java b/dspace-api/src/test/java/org/dspace/content/logic/LogicalFilterTest.java index 0e0864622043..c84665f29853 100644 --- a/dspace-api/src/test/java/org/dspace/content/logic/LogicalFilterTest.java +++ b/dspace-api/src/test/java/org/dspace/content/logic/LogicalFilterTest.java @@ -57,8 +57,10 @@ import org.dspace.content.service.MetadataValueService; import org.dspace.content.service.WorkspaceItemService; import org.dspace.core.Constants; +import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.eperson.factory.EPersonServiceFactory; +import org.dspace.eperson.service.EPersonService; import org.dspace.eperson.service.GroupService; import org.junit.After; import org.junit.Before; @@ -81,6 +83,7 @@ public class LogicalFilterTest extends AbstractUnitTest { private MetadataValueService metadataValueService = ContentServiceFactory.getInstance().getMetadataValueService(); private AuthorizeService authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService(); private GroupService groupService = EPersonServiceFactory.getInstance().getGroupService(); + private EPersonService epersonService = EPersonServiceFactory.getInstance().getEPersonService(); // Logger private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(LogicalFilterTest.class); @@ -603,6 +606,10 @@ public void testReadableByGroupCondition() { groupService.setName(g, "Test Group"); groupService.update(context, g); authorizeService.addPolicy(context, itemOne, Constants.READ, g); + EPerson e = epersonService.create(context); + epersonService.update(context, e); + authorizeService.removeAllPolicies(context, itemThree); + authorizeService.addPolicy(context, itemThree, Constants.READ, e); context.restoreAuthSystemState(); } catch (AuthorizeException | SQLException e) { fail("Exception thrown adding group READ policy to item: " + itemOne + ": " + e.getMessage()); @@ -620,6 +627,9 @@ public void testReadableByGroupCondition() { // Test the filter on itemTwo - this item has no policies: expect false assertFalse("itemTwo unexpectedly matched the 'is readable by Test Group' test", filter.getResult(context, itemTwo)); + // Test the filter on itemThree - this item has only a person related policy: expect false + assertFalse("itemThree unexpectedly matched the 'is readable by Test Group' test", + filter.getResult(context, itemThree)); } catch (LogicalStatementException e) { log.error(e.getMessage()); fail("LogicalStatementException thrown testing the ReadableByGroup filter" + e.getMessage()); 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 02154e715c55..f4c2b13b9943 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 @@ -685,8 +685,8 @@ public void testDeleteItemWithMultipleVersions() throws Exception { @Test public void testFindItemsWithEditNoRights() throws Exception { context.setCurrentUser(eperson); - List result = itemService.findItemsWithEdit(context, 0, 10); - int count = itemService.countItemsWithEdit(context); + List result = itemService.findItemsWithEdit(context, "", 0, 10); + int count = itemService.countItemsWithEdit(context, ""); assertThat(result.size(), equalTo(0)); assertThat(count, equalTo(0)); } @@ -698,8 +698,8 @@ public void testFindAndCountItemsWithEditEPerson() throws Exception { .withAction(Constants.WRITE) .build(); context.setCurrentUser(eperson); - List result = itemService.findItemsWithEdit(context, 0, 10); - int count = itemService.countItemsWithEdit(context); + List result = itemService.findItemsWithEdit(context, "", 0, 10); + int count = itemService.countItemsWithEdit(context, ""); assertThat(result.size(), equalTo(1)); assertThat(count, equalTo(1)); } @@ -711,8 +711,8 @@ public void testFindAndCountItemsWithAdminEPerson() throws Exception { .withAction(Constants.ADMIN) .build(); context.setCurrentUser(eperson); - List result = itemService.findItemsWithEdit(context, 0, 10); - int count = itemService.countItemsWithEdit(context); + List result = itemService.findItemsWithEdit(context, "", 0, 10); + int count = itemService.countItemsWithEdit(context, ""); assertThat(result.size(), equalTo(1)); assertThat(count, equalTo(1)); } @@ -730,8 +730,8 @@ public void testFindAndCountItemsWithEditGroup() throws Exception { .withAction(Constants.WRITE) .build(); context.setCurrentUser(eperson); - List result = itemService.findItemsWithEdit(context, 0, 10); - int count = itemService.countItemsWithEdit(context); + List result = itemService.findItemsWithEdit(context, "", 0, 10); + int count = itemService.countItemsWithEdit(context, ""); assertThat(result.size(), equalTo(1)); assertThat(count, equalTo(1)); } @@ -749,8 +749,8 @@ public void testFindAndCountItemsWithAdminGroup() throws Exception { .withAction(Constants.ADMIN) .build(); context.setCurrentUser(eperson); - List result = itemService.findItemsWithEdit(context, 0, 10); - int count = itemService.countItemsWithEdit(context); + List result = itemService.findItemsWithEdit(context, "", 0, 10); + int count = itemService.countItemsWithEdit(context, ""); assertThat(result.size(), equalTo(1)); assertThat(count, equalTo(1)); } 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 3b50258a5a23..5013f2cb6690 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 @@ -8,8 +8,12 @@ package org.dspace.ctask.general; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; @@ -19,6 +23,7 @@ import org.dspace.core.factory.CoreServiceFactory; import org.dspace.curate.Curator; import org.dspace.identifier.AbstractIdentifierProviderIT; +import org.dspace.identifier.IdentifierProvider; import org.dspace.identifier.VersionedHandleIdentifierProvider; import org.dspace.identifier.VersionedHandleIdentifierProviderWithCanonicalHandles; import org.dspace.services.ConfigurationService; @@ -45,6 +50,7 @@ public void testPerform() // Must remove any cached named plugins before creating a new one CoreServiceFactory.getInstance().getPluginService().clearNamedPluginClasses(); // Define a new task dynamically + String[] prevTaskDef = configurationService.getArrayProperty(P_TASK_DEF); configurationService.setProperty(P_TASK_DEF, CreateMissingIdentifiers.class.getCanonicalName() + " = " + TASK_NAME); @@ -82,5 +88,64 @@ public void testPerform() curator.curate(context, item); int status = curator.getStatus(TASK_NAME); assertEquals("Curation should succeed", Curator.CURATE_SUCCESS, status); + configurationService.setProperty(P_TASK_DEF, prevTaskDef); + } + + @Test + public void testCreationOfMissingHandles() throws IOException { + // Must remove any cached named plugins before creating a new one + CoreServiceFactory.getInstance().getPluginService().clearNamedPluginClasses(); + // Define a new task dynamically + String[] prevTaskDef = configurationService.getArrayProperty(P_TASK_DEF); + configurationService.setProperty(P_TASK_DEF, + CreateMissingIdentifiers.class.getCanonicalName() + " = " + TASK_NAME); + + // deactivate all identifier provider + List identifierProviders = identifierService.getProviders(); + List identifierProviderClasses = + identifierProviders.stream().map(Object::getClass).distinct().collect(Collectors.toList()); + for (Class identifierProviderClass : identifierProviderClasses) { + unregisterProvider(identifierProviderClass); + } + + try { + context.setCurrentUser(admin); + parentCommunity = CommunityBuilder.createCommunity(context) + .build(); + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .build(); + // create item and assert it did not got any handle + Item item = ItemBuilder.createItem(context, collection) + .build(); + assertNull("Internal error in createMissingIdentifiersIT: item should not have a handle", + item.getHandle()); + + // setup the curator + Curator curator = new Curator(); + curator.addTask(TASK_NAME); + + // register the default Handle Provider + registerProvider(VersionedHandleIdentifierProvider.class); + + /* + * Now, verify curate with default Handle Provider works + * (and that our re-registration of the default provider above was successful) + * Use the uuid as reference to the item as the curation system takes handles and uuid as cli arguments. + * Do not use the item reference, to be sure the curator and the curation task are able to work without + * handle. + */ + curator.curate(context, item.getID().toString()); + int status = curator.getStatus(TASK_NAME); + assertEquals("Curation should succeed", Curator.CURATE_SUCCESS, status); + // assure we got a handle + assertNotNull("Curation task CreateMissingIdentifiers, did not assign a handle.", item.getHandle()); + } finally { + // restore the identifierProviders for following tests + for (Class identifierProviderClass : identifierProviderClasses) { + registerProvider(identifierProviderClass); + } + // restore curation task configuration + configurationService.setProperty(P_TASK_DEF, prevTaskDef); + } } } diff --git a/dspace-api/src/test/java/org/dspace/external/OpenAIRERestConnectorTest.java b/dspace-api/src/test/java/org/dspace/external/OpenAIRERestConnectorTest.java new file mode 100644 index 000000000000..2cb409c4b7fd --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/external/OpenAIRERestConnectorTest.java @@ -0,0 +1,61 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.external; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import eu.openaire.jaxb.model.Response; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.dspace.app.client.DSpaceHttpClientFactory; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + + +public class OpenAIRERestConnectorTest { + + @Test + public void searchProjectByKeywords() throws IOException { + try (InputStream is = this.getClass().getResourceAsStream("openaire-projects.xml"); + MockWebServer mockServer = new MockWebServer()) { + String projects = new String(is.readAllBytes(), StandardCharsets.UTF_8) + .replaceAll("( mushroom)", "( DEADBEEF)"); + mockServer.enqueue(new MockResponse().setResponseCode(200).setBody(projects)); + + // setup mocks so we don't have to set whole DSpace kernel etc. + // still, the idea is to test how the get method behaves + CloseableHttpClient httpClient = spy(HttpClientBuilder.create().build()); + doReturn(httpClient.execute(new HttpGet(mockServer.url("").toString()))) + .when(httpClient).execute(Mockito.any()); + + DSpaceHttpClientFactory mock = Mockito.mock(DSpaceHttpClientFactory.class); + when(mock.build()).thenReturn(httpClient); + + try (MockedStatic mockedFactory = + Mockito.mockStatic(DSpaceHttpClientFactory.class)) { + mockedFactory.when(DSpaceHttpClientFactory::getInstance).thenReturn(mock); + OpenaireRestConnector connector = new OpenaireRestConnector(mockServer.url("").toString()); + Response response = connector.searchProjectByKeywords(0, 10, "keyword"); + // Basically check it doesn't throw UnmarshallerException and that we are getting our mocked response + assertTrue("Expected the query to contain the replaced keyword", + response.getHeader().getQuery().contains("DEADBEEF")); + } + } + } +} diff --git a/dspace-api/src/test/java/org/dspace/external/provider/impl/OpenaireFundingDataProviderTest.java b/dspace-api/src/test/java/org/dspace/external/provider/impl/OpenaireFundingDataProviderTest.java index d14dc990353d..fd417f3153e2 100644 --- a/dspace-api/src/test/java/org/dspace/external/provider/impl/OpenaireFundingDataProviderTest.java +++ b/dspace-api/src/test/java/org/dspace/external/provider/impl/OpenaireFundingDataProviderTest.java @@ -14,7 +14,9 @@ import java.util.List; import java.util.Optional; +import eu.openaire.jaxb.model.Response; import org.dspace.AbstractDSpaceTest; +import org.dspace.external.OpenaireRestConnector; import org.dspace.external.factory.ExternalServiceFactory; import org.dspace.external.model.ExternalDataObject; import org.dspace.external.provider.ExternalDataProvider; @@ -102,4 +104,21 @@ public void testGetDataObjectWInvalidId() { assertTrue("openaireFunding.getExternalDataObject.notExists:WRONGID", result.isEmpty()); } + + @Test + public void testGetNumberOfResultsWhenResponseIsNull() { + // Create a mock connector that returns null + OpenaireFundingDataProvider provider = new OpenaireFundingDataProvider(); + provider.setSourceIdentifier("test"); + provider.setConnector(new OpenaireRestConnector("test") { + @Override + public Response searchProjectByKeywords(int page, int size, String... keywords) { + return null; + } + }); + + // Should return 0 when response is null, not throw NullPointerException + int result = provider.getNumberOfResults("test"); + assertEquals("Should return 0 when response is null", 0, result); + } } diff --git a/dspace-api/src/test/java/org/dspace/orcid/service/OrcidEntityFactoryServiceIT.java b/dspace-api/src/test/java/org/dspace/orcid/service/OrcidEntityFactoryServiceIT.java index 912efcfcf323..c73e7adecc41 100644 --- a/dspace-api/src/test/java/org/dspace/orcid/service/OrcidEntityFactoryServiceIT.java +++ b/dspace-api/src/test/java/org/dspace/orcid/service/OrcidEntityFactoryServiceIT.java @@ -73,6 +73,9 @@ public class OrcidEntityFactoryServiceIT extends AbstractIntegrationTestWithData private Collection projects; + private static final String isbn = "978-0-439-02348-1"; + private static final String issn = "1234-1234X"; + @Before public void setup() { @@ -117,6 +120,7 @@ public void testWorkCreation() { .withLanguage("en_US") .withType("Book") .withIsPartOf("Journal") + .withISBN(isbn) .withDoiIdentifier("doi-id") .withScopusIdentifier("scopus-id") .build(); @@ -149,11 +153,100 @@ public void testWorkCreation() { assertThat(work.getExternalIdentifiers(), notNullValue()); List externalIds = work.getExternalIdentifiers().getExternalIdentifier(); - assertThat(externalIds, hasSize(3)); + assertThat(externalIds, hasSize(4)); + assertThat(externalIds, has(selfExternalId("doi", "doi-id"))); + assertThat(externalIds, has(selfExternalId("eid", "scopus-id"))); + assertThat(externalIds, has(selfExternalId("handle", publication.getHandle()))); + // Book type should have SELF rel for ISBN + assertThat(externalIds, has(selfExternalId("isbn", isbn))); + + } + + @Test + public void testJournalArticleAndISSN() { + context.turnOffAuthorisationSystem(); + + Item publication = ItemBuilder.createItem(context, publications) + .withTitle("Test publication") + .withAuthor("Walter White") + .withAuthor("Jesse Pinkman") + .withEditor("Editor") + .withIssueDate("2021-04-30") + .withDescriptionAbstract("Publication description") + .withLanguage("en_US") + .withType("Article") + .withIsPartOf("Journal") + .withISSN(issn) + .withDoiIdentifier("doi-id") + .withScopusIdentifier("scopus-id") + .build(); + + context.restoreAuthSystemState(); + + Activity activity = entityFactoryService.createOrcidObject(context, publication); + assertThat(activity, instanceOf(Work.class)); + + Work work = (Work) activity; + assertThat(work.getJournalTitle(), notNullValue()); + assertThat(work.getJournalTitle().getContent(), is("Journal")); + assertThat(work.getLanguageCode(), is("en")); + assertThat(work.getPublicationDate(), matches(date("2021", "04", "30"))); + assertThat(work.getShortDescription(), is("Publication description")); + assertThat(work.getPutCode(), nullValue()); + assertThat(work.getWorkType(), is(WorkType.JOURNAL_ARTICLE)); + assertThat(work.getWorkTitle(), notNullValue()); + assertThat(work.getWorkTitle().getTitle(), notNullValue()); + assertThat(work.getWorkTitle().getTitle().getContent(), is("Test publication")); + assertThat(work.getWorkContributors(), notNullValue()); + assertThat(work.getUrl(), matches(urlEndsWith(publication.getHandle()))); + + List contributors = work.getWorkContributors().getContributor(); + assertThat(contributors, hasSize(3)); + assertThat(contributors, has(contributor("Walter White", AUTHOR, FIRST))); + assertThat(contributors, has(contributor("Editor", EDITOR, FIRST))); + assertThat(contributors, has(contributor("Jesse Pinkman", AUTHOR, ADDITIONAL))); + + assertThat(work.getExternalIdentifiers(), notNullValue()); + + List externalIds = work.getExternalIdentifiers().getExternalIdentifier(); + assertThat(externalIds, hasSize(4)); assertThat(externalIds, has(selfExternalId("doi", "doi-id"))); assertThat(externalIds, has(selfExternalId("eid", "scopus-id"))); assertThat(externalIds, has(selfExternalId("handle", publication.getHandle()))); + // journal-article should have PART_OF rel for ISSN + assertThat(externalIds, has(externalId("issn", issn, Relationship.PART_OF))); + } + @Test + public void testJournalWithISSN() { + context.turnOffAuthorisationSystem(); + + Item publication = ItemBuilder.createItem(context, publications) + .withTitle("Test journal") + .withEditor("Editor") + .withType("Journal") + .withISSN(issn) + .build(); + + context.restoreAuthSystemState(); + + Activity activity = entityFactoryService.createOrcidObject(context, publication); + assertThat(activity, instanceOf(Work.class)); + + Work work = (Work) activity; + assertThat(work.getWorkType(), is(WorkType.JOURNAL_ISSUE)); + assertThat(work.getWorkTitle(), notNullValue()); + assertThat(work.getWorkTitle().getTitle(), notNullValue()); + assertThat(work.getWorkTitle().getTitle().getContent(), is("Test journal")); + assertThat(work.getUrl(), matches(urlEndsWith(publication.getHandle()))); + + assertThat(work.getExternalIdentifiers(), notNullValue()); + + List externalIds = work.getExternalIdentifiers().getExternalIdentifier(); + assertThat(externalIds, hasSize(2)); + // journal-issue should have SELF rel for ISSN + assertThat(externalIds, has(selfExternalId("issn", issn))); + assertThat(externalIds, has(selfExternalId("handle", publication.getHandle()))); } @Test @@ -163,6 +256,7 @@ public void testEmptyWorkWithUnknownTypeCreation() { Item publication = ItemBuilder.createItem(context, publications) .withType("TYPE") + .withISSN(issn) .build(); context.restoreAuthSystemState(); @@ -183,8 +277,9 @@ public void testEmptyWorkWithUnknownTypeCreation() { assertThat(work.getExternalIdentifiers(), notNullValue()); List externalIds = work.getExternalIdentifiers().getExternalIdentifier(); - assertThat(externalIds, hasSize(1)); + assertThat(externalIds, hasSize(2)); assertThat(externalIds, has(selfExternalId("handle", publication.getHandle()))); + assertThat(externalIds, has(externalId("issn", issn, Relationship.PART_OF))); } @Test diff --git a/dspace-api/src/test/java/org/dspace/statistics/SolrLoggerServiceImplIT.java b/dspace-api/src/test/java/org/dspace/statistics/SolrLoggerServiceImplIT.java index 82f8680aea27..956ba81f4931 100644 --- a/dspace-api/src/test/java/org/dspace/statistics/SolrLoggerServiceImplIT.java +++ b/dspace-api/src/test/java/org/dspace/statistics/SolrLoggerServiceImplIT.java @@ -12,6 +12,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.Writer; import java.nio.charset.StandardCharsets; @@ -29,8 +30,14 @@ import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrInputDocument; import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.builder.BitstreamBuilder; +import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.content.Bitstream; +import org.dspace.content.Collection; import org.dspace.content.Community; +import org.dspace.content.Item; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.core.Constants; import org.dspace.core.factory.CoreServiceFactory; @@ -305,4 +312,56 @@ public void testDeleteRobots() } assertEquals("Wrong number of documents remaining --", 1, nDocs); } + + @Test + public void testPostViewShouldNotLogIgnoredBundles() throws Exception { + ContentServiceFactory csf = ContentServiceFactory.getInstance(); + MockSolrLoggerServiceImpl solrLoggerService = DSpaceServicesFactory.getInstance() + .getServiceManager() + .getServiceByName("solrLoggerService", MockSolrLoggerServiceImpl.class); + solrLoggerService.bitstreamService = csf.getBitstreamService(); + solrLoggerService.contentServiceFactory = csf; + solrLoggerService.configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + solrLoggerService.clientInfoService = CoreServiceFactory.getInstance().getClientInfoService(); + solrLoggerService.afterPropertiesSet(); + SolrStatisticsCore solrStatisticsCore = DSpaceServicesFactory.getInstance() + .getServiceManager() + .getServiceByName(SolrStatisticsCore.class.getName(), MockSolrStatisticsCore.class); + + solrStatisticsCore.getSolr().deleteByQuery("*:*"); + solrStatisticsCore.getSolr().commit(); + + context.turnOffAuthorisationSystem(); + Community community = CommunityBuilder + .createCommunity(context) + .withName("Test Community").build(); + Collection collection = CollectionBuilder + .createCollection(context, community) + .withName("Test Collection").build(); + Item item = ItemBuilder + .createItem(context, collection) + .withTitle("Test Item for Logging").build(); + Bitstream originalBitstream = BitstreamBuilder + .createBitstream(context, item, new ByteArrayInputStream("original content".getBytes()), "ORIGINAL") + .withName("original.txt") + .build(); + Bitstream thumbnailBitstream = BitstreamBuilder + .createBitstream(context, item, new ByteArrayInputStream("thumbnail content".getBytes()), "THUMBNAIL") + .withName("thumbnail.jpg") + .build(); + + context.restoreAuthSystemState(); + solrLoggerService.postView(originalBitstream, null, eperson); + solrLoggerService.postView(thumbnailBitstream, null, eperson); + + solrStatisticsCore.getSolr().commit(); + + SolrQuery thumbnailQuery = new SolrQuery("id:" + thumbnailBitstream.getID().toString()); + QueryResponse thumbnailResponse = solrStatisticsCore.getSolr().query(thumbnailQuery); + assertEquals("Thumbnail bundle should NOT be logged", 0, thumbnailResponse.getResults().getNumFound()); + + SolrQuery originalQuery = new SolrQuery("id:" + originalBitstream.getID().toString()); + QueryResponse originalResponse = solrStatisticsCore.getSolr().query(originalQuery); + assertEquals("ORIGINAL bundle SHOULD be logged", 1, originalResponse.getResults().getNumFound()); + } } diff --git a/dspace-api/src/test/java/org/dspace/statistics/export/ITIrusExportUsageEventListener.java b/dspace-api/src/test/java/org/dspace/statistics/export/ITIrusExportUsageEventListener.java index 0c861a0d293d..48cf0b14b6bd 100644 --- a/dspace-api/src/test/java/org/dspace/statistics/export/ITIrusExportUsageEventListener.java +++ b/dspace-api/src/test/java/org/dspace/statistics/export/ITIrusExportUsageEventListener.java @@ -116,6 +116,7 @@ public void setUp() throws Exception { configurationService.setProperty("irus.statistics.tracker.enabled", true); configurationService.setProperty("irus.statistics.tracker.type-field", "dc.type"); configurationService.setProperty("irus.statistics.tracker.type-value", "Excluded type"); + configurationService.setProperty("oai.identifier.prefix", "localhost"); context.turnOffAuthorisationSystem(); diff --git a/dspace-api/src/test/java/org/dspace/statistics/export/processor/ExportEventProcessorIT.java b/dspace-api/src/test/java/org/dspace/statistics/export/processor/ExportEventProcessorIT.java index fb53d0c83c54..96909a9e3dbd 100644 --- a/dspace-api/src/test/java/org/dspace/statistics/export/processor/ExportEventProcessorIT.java +++ b/dspace-api/src/test/java/org/dspace/statistics/export/processor/ExportEventProcessorIT.java @@ -62,6 +62,7 @@ public void setUp() throws Exception { configurationService.setProperty("irus.statistics.tracker.enabled", true); configurationService.setProperty("irus.statistics.tracker.type-field", "dc.type"); configurationService.setProperty("irus.statistics.tracker.type-value", "Excluded type"); + configurationService.setProperty("oai.identifier.prefix", "localhost"); context.turnOffAuthorisationSystem(); publication = EntityTypeBuilder.createEntityTypeBuilder(context, "Publication").build(); diff --git a/dspace-api/src/test/java/org/dspace/storage/bitstore/BitstreamStorageServiceImplIT.java b/dspace-api/src/test/java/org/dspace/storage/bitstore/BitstreamStorageServiceImplIT.java index ca3ac768a353..5b15cba1c4c1 100644 --- a/dspace-api/src/test/java/org/dspace/storage/bitstore/BitstreamStorageServiceImplIT.java +++ b/dspace-api/src/test/java/org/dspace/storage/bitstore/BitstreamStorageServiceImplIT.java @@ -38,13 +38,6 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; -/** - * UMD Customization - * - * This class was provided to DSpace in Pull Request 10940 - * This comment can be removed once this application has been upgraded to a - * DSpace version containing the pull request. - */ public class BitstreamStorageServiceImplIT extends AbstractIntegrationTestWithDatabase { private BitstreamService bitstreamService = ContentServiceFactory.getInstance().getBitstreamService(); private BitstreamStorageServiceImpl bitstreamStorageService = diff --git a/dspace-api/src/test/java/org/dspace/storage/bitstore/S3BitStoreServiceIT.java b/dspace-api/src/test/java/org/dspace/storage/bitstore/S3BitStoreServiceIT.java index 6ea21eac8d6d..37c2b1e0dd2a 100644 --- a/dspace-api/src/test/java/org/dspace/storage/bitstore/S3BitStoreServiceIT.java +++ b/dspace-api/src/test/java/org/dspace/storage/bitstore/S3BitStoreServiceIT.java @@ -7,13 +7,12 @@ */ package org.dspace.storage.bitstore; -import static com.amazonaws.regions.Regions.DEFAULT_REGION; import static java.nio.charset.StandardCharsets.UTF_8; import static org.dspace.storage.bitstore.S3BitStoreService.CSA; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; @@ -26,22 +25,16 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.net.URI; +import java.nio.file.Paths; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.sql.SQLException; +import java.util.ArrayList; import java.util.List; import java.util.Map; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.AnonymousAWSCredentials; -import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; -import com.amazonaws.services.s3.model.AmazonS3Exception; -import com.amazonaws.services.s3.model.Bucket; -import com.amazonaws.services.s3.model.ObjectMetadata; -import io.findify.s3mock.S3Mock; -import org.apache.commons.io.FileUtils; +import com.adobe.testing.s3mock.testcontainers.S3MockContainer; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.BooleanUtils; import org.dspace.AbstractIntegrationTestWithDatabase; @@ -59,47 +52,64 @@ import org.dspace.services.factory.DSpaceServicesFactory; import org.hamcrest.Matcher; import org.hamcrest.Matchers; -import org.junit.After; +import org.junit.AfterClass; import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Test; - - +import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.Bucket; +import software.amazon.awssdk.services.s3.model.ChecksumAlgorithm; +import software.amazon.awssdk.services.s3.model.HeadObjectResponse; /** * @author Luca Giamminonni (luca.giamminonni at 4science.com) */ +// UMD Customization +@Ignore("UMD - These tests consistently fail when run on Jenkins, see https://github.com/DSpace/DSpace/pull/11900") +// End UMD Customization public class S3BitStoreServiceIT extends AbstractIntegrationTestWithDatabase { + private static S3MockContainer s3Mock = new S3MockContainer("4.8.0"); + + private static S3AsyncClient s3AsyncClient; private static final String DEFAULT_BUCKET_NAME = "dspace-asset-localhost"; private S3BitStoreService s3BitStoreService; - private AmazonS3 amazonS3Client; - - private S3Mock s3Mock; - private Collection collection; - private File s3Directory; - private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + @BeforeClass + public static void setupS3() { + s3Mock.start(); + + s3AsyncClient = S3AsyncClient.crtBuilder() + .endpointOverride(URI.create("http://127.0.0.1:" + s3Mock.getHttpServerPort())) + .credentialsProvider(AnonymousCredentialsProvider.create()) + .region(Region.US_EAST_1) + .build(); + } + + @AfterClass + public static void cleanupS3() { + s3Mock.close(); + s3AsyncClient.close(); + } @Before public void setup() throws Exception { - configurationService.setProperty("assetstore.s3.enabled", "true"); - s3Directory = new File(System.getProperty("java.io.tmpdir"), "s3"); - - s3Mock = S3Mock.create(8001, s3Directory.getAbsolutePath()); - s3Mock.start(); - amazonS3Client = createAmazonS3Client(); - - s3BitStoreService = new S3BitStoreService(amazonS3Client); + s3BitStoreService = new S3BitStoreService(s3AsyncClient); s3BitStoreService.setEnabled(BooleanUtils.toBoolean( configurationService.getProperty("assetstore.s3.enabled"))); - s3BitStoreService.setBufferSize(22); + s3BitStoreService.setS3ChecksumAlgorithm(ChecksumAlgorithm.SHA256); + context.turnOffAuthorisationSystem(); parentCommunity = CommunityBuilder.createCommunity(context) @@ -111,23 +121,17 @@ public void setup() throws Exception { context.restoreAuthSystemState(); } - @After - public void cleanUp() throws IOException { - FileUtils.deleteDirectory(s3Directory); - s3Mock.shutdown(); - } - @Test public void testBitstreamPutAndGetWithAlreadyPresentBucket() throws IOException { String bucketName = "testbucket"; - amazonS3Client.createBucket(bucketName); + s3AsyncClient.createBucket(r -> r.bucket(bucketName)).join(); s3BitStoreService.setBucketName(bucketName); s3BitStoreService.init(); - assertThat(amazonS3Client.listBuckets(), contains(bucketNamed(bucketName))); + assertThat(s3AsyncClient.listBuckets().join().buckets(), hasItem(bucketNamed(bucketName))); context.turnOffAuthorisationSystem(); String content = "Test bitstream content"; @@ -149,7 +153,7 @@ public void testBitstreamPutAndGetWithAlreadyPresentBucket() throws IOException private void checkGetPut(String bucketName, String content, Bitstream bitstream) throws IOException { s3BitStoreService.put(bitstream, toInputStream(content)); - String expectedChecksum = Utils.toHex(generateChecksum(content)); + String expectedChecksum = Utils.toHex(generateChecksum("MD5", content)); assertThat(bitstream.getSizeBytes(), is((long) content.length())); assertThat(bitstream.getChecksum(), is(expectedChecksum)); @@ -157,20 +161,16 @@ private void checkGetPut(String bucketName, String content, Bitstream bitstream) InputStream inputStream = s3BitStoreService.get(bitstream); assertThat(IOUtils.toString(inputStream, UTF_8), is(content)); - - String key = s3BitStoreService.getFullKey(bitstream.getInternalId()); - ObjectMetadata objectMetadata = amazonS3Client.getObjectMetadata(bucketName, key); - assertThat(objectMetadata.getContentMD5(), is(expectedChecksum)); } @Test - public void testBitstreamPutAndGetWithoutSpecifingBucket() throws IOException { + public void testBitstreamPutAndGetWithoutSpecifyingBucket() throws IOException { s3BitStoreService.init(); assertThat(s3BitStoreService.getBucketName(), is(DEFAULT_BUCKET_NAME)); - assertThat(amazonS3Client.listBuckets(), contains(bucketNamed(DEFAULT_BUCKET_NAME))); + assertThat(s3AsyncClient.listBuckets().join().buckets(), hasItem(bucketNamed(DEFAULT_BUCKET_NAME))); context.turnOffAuthorisationSystem(); String content = "Test bitstream content"; @@ -179,7 +179,7 @@ public void testBitstreamPutAndGetWithoutSpecifingBucket() throws IOException { s3BitStoreService.put(bitstream, toInputStream(content)); - String expectedChecksum = Utils.toHex(generateChecksum(content)); + String expectedChecksum = Utils.toHex(generateChecksum("MD5", content)); assertThat(bitstream.getSizeBytes(), is((long) content.length())); assertThat(bitstream.getChecksum(), is(expectedChecksum)); @@ -187,11 +187,6 @@ public void testBitstreamPutAndGetWithoutSpecifingBucket() throws IOException { InputStream inputStream = s3BitStoreService.get(bitstream); assertThat(IOUtils.toString(inputStream, UTF_8), is(content)); - - String key = s3BitStoreService.getFullKey(bitstream.getInternalId()); - ObjectMetadata objectMetadata = amazonS3Client.getObjectMetadata(DEFAULT_BUCKET_NAME, key); - assertThat(objectMetadata.getContentMD5(), is(expectedChecksum)); - } @Test @@ -213,9 +208,9 @@ public void testBitstreamPutAndGetWithSubFolder() throws IOException { String key = s3BitStoreService.getFullKey(bitstream.getInternalId()); assertThat(key, startsWith("test/DSpace7/")); - ObjectMetadata objectMetadata = amazonS3Client.getObjectMetadata(DEFAULT_BUCKET_NAME, key); - assertThat(objectMetadata, notNullValue()); - + HeadObjectResponse response = s3AsyncClient.headObject(r -> + r.bucket(DEFAULT_BUCKET_NAME).key(key)).join(); + assertThat(response, notNullValue()); } @Test @@ -235,8 +230,8 @@ public void testBitstreamDeletion() throws IOException { s3BitStoreService.remove(bitstream); IOException exception = assertThrows(IOException.class, () -> s3BitStoreService.get(bitstream)); - assertThat(exception.getCause(), instanceOf(AmazonS3Exception.class)); - assertThat(((AmazonS3Exception) exception.getCause()).getStatusCode(), is(404)); + assertThat(exception.getCause(), instanceOf(AwsServiceException.class)); + assertThat(((AwsServiceException) exception.getCause()).statusCode(), is(404)); } @@ -264,7 +259,7 @@ public void testAbout() throws IOException { assertThat(about, hasEntry(is("modified"), notNullValue())); assertThat(about.size(), is(2)); - String expectedChecksum = Utils.toHex(generateChecksum(content)); + String expectedChecksum = Utils.toHex(generateChecksum("MD5", content)); about = s3BitStoreService.about(bitstream, List.of("size_bytes", "modified", "checksum")); assertThat(about, hasEntry("size_bytes", 22L)); @@ -337,7 +332,7 @@ public void givenBitStreamIdentifierWhenIntermediatePathIsComputedThenNotEndingD String computedPath = this.s3BitStoreService.getIntermediatePath(path.toString()); int slashes = computeSlashes(path.toString()); assertThat(computedPath, Matchers.endsWith(File.separator)); - assertThat(computedPath.split(File.separator).length, Matchers.equalTo(slashes)); + assertThat(countPathElements(computedPath), Matchers.equalTo(slashes)); path.append("2"); computedPath = this.s3BitStoreService.getIntermediatePath(path.toString()); @@ -362,31 +357,31 @@ public void givenBitStreamIdentidierWhenIntermediatePathIsComputedThenMustBeSpli String computedPath = this.s3BitStoreService.getIntermediatePath(path.toString()); int slashes = computeSlashes(path.toString()); assertThat(computedPath, Matchers.endsWith(File.separator)); - assertThat(computedPath.split(File.separator).length, Matchers.equalTo(slashes)); + assertThat(countPathElements(computedPath), Matchers.equalTo(slashes)); path.append("2"); computedPath = this.s3BitStoreService.getIntermediatePath(path.toString()); slashes = computeSlashes(path.toString()); assertThat(computedPath, Matchers.endsWith(File.separator)); - assertThat(computedPath.split(File.separator).length, Matchers.equalTo(slashes)); + assertThat(countPathElements(computedPath), Matchers.equalTo(slashes)); path.append("3"); computedPath = this.s3BitStoreService.getIntermediatePath(path.toString()); slashes = computeSlashes(path.toString()); assertThat(computedPath, Matchers.endsWith(File.separator)); - assertThat(computedPath.split(File.separator).length, Matchers.equalTo(slashes)); + assertThat(countPathElements(computedPath), Matchers.equalTo(slashes)); path.append("4"); computedPath = this.s3BitStoreService.getIntermediatePath(path.toString()); slashes = computeSlashes(path.toString()); assertThat(computedPath, Matchers.endsWith(File.separator)); - assertThat(computedPath.split(File.separator).length, Matchers.equalTo(slashes)); + assertThat(countPathElements(computedPath), Matchers.equalTo(slashes)); path.append("56789"); computedPath = this.s3BitStoreService.getIntermediatePath(path.toString()); slashes = computeSlashes(path.toString()); assertThat(computedPath, Matchers.endsWith(File.separator)); - assertThat(computedPath.split(File.separator).length, Matchers.equalTo(slashes)); + assertThat(countPathElements(computedPath), Matchers.equalTo(slashes)); } @Test @@ -409,16 +404,16 @@ public void givenBitStreamIdentifierWithSlashesWhenSanitizedThenSlashesMustBeRem public void testDoNotInitializeConfigured() throws Exception { String assetstores3enabledOldValue = configurationService.getProperty("assetstore.s3.enabled"); configurationService.setProperty("assetstore.s3.enabled", "false"); - s3BitStoreService = new S3BitStoreService(amazonS3Client); + s3BitStoreService = new S3BitStoreService(s3AsyncClient); s3BitStoreService.init(); assertFalse(s3BitStoreService.isInitialized()); assertFalse(s3BitStoreService.isEnabled()); configurationService.setProperty("assetstore.s3.enabled", assetstores3enabledOldValue); } - private byte[] generateChecksum(String content) { + private byte[] generateChecksum(String algorithm, String content) { try { - MessageDigest m = MessageDigest.getInstance("MD5"); + MessageDigest m = MessageDigest.getInstance(algorithm); m.update(content.getBytes()); return m.digest(); } catch (NoSuchAlgorithmException e) { @@ -426,13 +421,6 @@ private byte[] generateChecksum(String content) { } } - private AmazonS3 createAmazonS3Client() { - return AmazonS3ClientBuilder.standard() - .withCredentials(new AWSStaticCredentialsProvider(new AnonymousAWSCredentials())) - .withEndpointConfiguration(new EndpointConfiguration("http://127.0.0.1:8001", DEFAULT_REGION.getName())) - .build(); - } - private Item createItem() { return ItemBuilder.createItem(context, collection) .withTitle("Test item") @@ -450,7 +438,7 @@ private Bitstream createBitstream(String content) { } private Matcher bucketNamed(String name) { - return LambdaMatcher.matches(bucket -> bucket.getName().equals(name)); + return LambdaMatcher.matches(bucket -> bucket.name().equals(name)); } private InputStream toInputStream(String content) { @@ -465,4 +453,12 @@ private int computeSlashes(String internalId) { return Math.min(slashes, S3BitStoreService.directoryLevels); } + // Count the number of elements in a Unix or Windows path. + // We use 'Paths' instead of splitting on slashes because these OSes use different path separators. + private int countPathElements(String stringPath) { + List pathElements = new ArrayList<>(); + Paths.get(stringPath).forEach(p -> pathElements.add(p.toString())); + return pathElements.size(); + } + } diff --git a/dspace-api/src/test/java/org/dspace/util/DSpaceConfigurationInitializer.java b/dspace-api/src/test/java/org/dspace/util/DSpaceConfigurationInitializer.java index e5a8adb2fdf7..e2e0355f123a 100644 --- a/dspace-api/src/test/java/org/dspace/util/DSpaceConfigurationInitializer.java +++ b/dspace-api/src/test/java/org/dspace/util/DSpaceConfigurationInitializer.java @@ -8,7 +8,7 @@ package org.dspace.util; import org.apache.commons.configuration2.Configuration; -import org.dspace.servicemanager.config.DSpaceConfigurationPropertySource; +import org.apache.commons.configuration2.spring.ConfigurationPropertySource; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; import org.springframework.context.ApplicationContextInitializer; @@ -38,8 +38,8 @@ public void initialize(final ConfigurableApplicationContext applicationContext) Configuration configuration = configurationService.getConfiguration(); // Create an Apache Commons Configuration Property Source from our configuration - DSpaceConfigurationPropertySource apacheCommonsConfigPropertySource = - new DSpaceConfigurationPropertySource(configuration.getClass().getName(), configuration); + ConfigurationPropertySource apacheCommonsConfigPropertySource = + new ConfigurationPropertySource(configuration.getClass().getName(), configuration); // Prepend it to the Environment's list of PropertySources // NOTE: This is added *first* in the list so that settings in DSpace's diff --git a/dspace-api/src/test/java/org/dspace/workflow/WorkflowCurationIT.java b/dspace-api/src/test/java/org/dspace/workflow/WorkflowCurationIT.java index d3866d534b24..37124bf6bc9c 100644 --- a/dspace-api/src/test/java/org/dspace/workflow/WorkflowCurationIT.java +++ b/dspace-api/src/test/java/org/dspace/workflow/WorkflowCurationIT.java @@ -22,6 +22,7 @@ import org.dspace.content.Community; import org.dspace.content.MetadataValue; import org.dspace.content.service.ItemService; +import org.dspace.core.LegacyPluginServiceImpl; import org.dspace.ctask.testing.MarkerTask; import org.dspace.eperson.EPerson; import org.dspace.util.DSpaceConfigurationInitializer; @@ -29,6 +30,7 @@ import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; @@ -46,6 +48,8 @@ public class WorkflowCurationIT extends AbstractIntegrationTestWithDatabase { @Inject private ItemService itemService; + @Autowired + private LegacyPluginServiceImpl legacyPluginService; /** * Basic smoke test of a curation task attached to a workflow step. @@ -56,6 +60,7 @@ public class WorkflowCurationIT public void curationTest() throws Exception { context.turnOffAuthorisationSystem(); + legacyPluginService.clearNamedPluginClasses(); //** GIVEN ** diff --git a/dspace-api/src/test/resources/org/dspace/app/mediafilter/cat-rotated-90.jpg b/dspace-api/src/test/resources/org/dspace/app/mediafilter/cat-rotated-90.jpg new file mode 100644 index 000000000000..5c0f91c4eda7 Binary files /dev/null and b/dspace-api/src/test/resources/org/dspace/app/mediafilter/cat-rotated-90.jpg differ diff --git a/dspace-api/src/test/resources/org/dspace/app/mediafilter/cat.jpg b/dspace-api/src/test/resources/org/dspace/app/mediafilter/cat.jpg new file mode 100644 index 000000000000..b282aa970c88 Binary files /dev/null and b/dspace-api/src/test/resources/org/dspace/app/mediafilter/cat.jpg differ diff --git a/dspace-iiif/pom.xml b/dspace-iiif/pom.xml index 179bc2f76e48..7b171a3918bd 100644 --- a/dspace-iiif/pom.xml +++ b/dspace-iiif/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT .. @@ -44,11 +44,6 @@ org.springframework.boot spring-boot-starter-logging - - - org.springframework - spring-jcl -
diff --git a/dspace-oai/pom.xml b/dspace-oai/pom.xml index 1e43427956d9..46590da35edd 100644 --- a/dspace-oai/pom.xml +++ b/dspace-oai/pom.xml @@ -8,7 +8,7 @@ dspace-parent org.dspace - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT .. @@ -78,11 +78,6 @@ org.springframework.boot spring-boot-starter-logging - - - org.springframework - spring-jcl - 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 c8babc036e3c..596e3a6bac23 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 @@ -190,9 +190,9 @@ private int index(Date last) throws DSpaceSolrIndexerException, IOException { .findInArchiveOrWithdrawnDiscoverableModifiedSince(context, last); Iterator nonDiscoverableChangedItems = itemService .findInArchiveOrWithdrawnNonDiscoverableModifiedSince(context, last); + int total = this.index(discoverableChangedItems, true) + this.index(nonDiscoverableChangedItems, true); Iterator possiblyChangedItems = getItemsWithPossibleChangesBefore(last); - return this.index(discoverableChangedItems) + this.index(nonDiscoverableChangedItems) - + this.index(possiblyChangedItems); + return total + this.index(possiblyChangedItems, false); } catch (SQLException ex) { throw new DSpaceSolrIndexerException(ex.getMessage(), ex); } @@ -262,7 +262,7 @@ private int indexAll() throws DSpaceSolrIndexerException { null); Iterator nonDiscoverableItems = itemService .findInArchiveOrWithdrawnNonDiscoverableModifiedSince(context, null); - return this.index(discoverableItems) + this.index(nonDiscoverableItems); + return this.index(discoverableItems, true) + this.index(nonDiscoverableItems, true); } catch (SQLException ex) { throw new DSpaceSolrIndexerException(ex.getMessage(), ex); } @@ -305,7 +305,7 @@ private boolean checkIfVisibleInOAI(Item item) throws IOException { } } - private int index(Iterator iterator) throws DSpaceSolrIndexerException { + private int index(Iterator iterator, boolean uncacheEntities) throws DSpaceSolrIndexerException { try { int i = 0; int batchSize = configurationService.getIntProperty("oai.import.batch.size", 1000); @@ -334,10 +334,12 @@ 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); + if (uncacheEntities) { + try { + context.uncacheEntities(); + } catch (SQLException ex) { + log.error("Error uncaching entities", ex); + } } } } diff --git a/dspace-oai/src/main/java/org/dspace/xoai/controller/DSpaceOAIDataProvider.java b/dspace-oai/src/main/java/org/dspace/xoai/controller/DSpaceOAIDataProvider.java index 3d826152c6ba..e070725de7be 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/controller/DSpaceOAIDataProvider.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/controller/DSpaceOAIDataProvider.java @@ -11,6 +11,8 @@ import static java.util.Arrays.asList; import static org.apache.logging.log4j.LogManager.getLogger; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Enumeration; @@ -18,6 +20,13 @@ import java.util.List; import java.util.Map; import javax.xml.stream.XMLStreamException; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; import com.lyncode.xoai.dataprovider.OAIDataProvider; import com.lyncode.xoai.dataprovider.OAIRequestParameters; @@ -25,11 +34,13 @@ import com.lyncode.xoai.dataprovider.exceptions.InvalidContextException; import com.lyncode.xoai.dataprovider.exceptions.OAIException; import com.lyncode.xoai.dataprovider.exceptions.WritingXmlException; +import jakarta.annotation.PostConstruct; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.apache.logging.log4j.Logger; import org.dspace.core.Context; +import org.dspace.services.ConfigurationService; import org.dspace.xoai.services.api.cache.XOAICacheService; import org.dspace.xoai.services.api.config.XOAIManagerResolver; import org.dspace.xoai.services.api.config.XOAIManagerResolverException; @@ -41,6 +52,10 @@ import org.dspace.xoai.services.impl.xoai.DSpaceResumptionTokenFormatter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.PathVariable; @@ -57,6 +72,9 @@ public class DSpaceOAIDataProvider { private static final Logger log = getLogger(DSpaceOAIDataProvider.class); + private TransformerFactory htmlTransformerFactory = null; + private byte[] htmlTransformerSource = null; + @Autowired XOAICacheService cacheService; @Autowired @@ -69,9 +87,32 @@ public class DSpaceOAIDataProvider { IdentifyResolver identifyResolver; @Autowired SetRepositoryResolver setRepositoryResolver; + @Autowired + ConfigurationService configurationService; private DSpaceResumptionTokenFormatter resumptionTokenFormat = new DSpaceResumptionTokenFormatter(); + @PostConstruct + public void setUpHTMLTransformerFactory() { + try { + XOAIManager manager = xoaiManagerResolver.getManager(); + if (configurationService.getBooleanProperty("oai.html", true) && manager.hasStyleSheet()) { + ResourceLoader resourceLoader = new DefaultResourceLoader(); + String styleSheetPath = manager.getStyleSheet(); + Resource styleSheetResource = resourceLoader.getResource("classpath:" + styleSheetPath); + htmlTransformerSource = styleSheetResource.getContentAsByteArray(); + htmlTransformerFactory = TransformerFactory.newInstance(); + // run for potential exceptions only as a sanity check on the XSLT: + htmlTransformerFactory.newTransformer( + new StreamSource(new ByteArrayInputStream(htmlTransformerSource))); + } + } catch (Exception e) { + htmlTransformerFactory = null; + htmlTransformerSource = null; + log.warn("Could not set up HTML transformer for OAI-PMH app: " + e.toString()); + } + } + @RequestMapping("") public void index(HttpServletResponse response, HttpServletRequest request) throws IOException { response.sendRedirect(request.getRequestURI() + "/"); @@ -92,7 +133,8 @@ public String indexAction(HttpServletResponse response, Model model) throws Serv @RequestMapping("/{context}") public String contextAction(Model model, HttpServletRequest request, HttpServletResponse response, - @PathVariable("context") String xoaiContext) throws IOException, ServletException { + @PathVariable("context") String xoaiContext) + throws IOException, ServletException, TransformerException { Context context = null; try { request.setCharacterEncoding("UTF-8"); @@ -109,7 +151,26 @@ public String contextAction(Model model, HttpServletRequest request, HttpServlet OutputStream out = response.getOutputStream(); OAIRequestParameters parameters = new OAIRequestParameters(buildParametersMap(request)); - response.setContentType("text/xml"); + boolean shouldServeAsHTML = false; + List acceptMediaTypes = MediaType.parseMediaTypes(request.getHeader("Accept")); + if (htmlTransformerFactory != null) { + response.addHeader("Vary", "Accept"); + for (MediaType acceptMediaType : acceptMediaTypes) { + if (acceptMediaType.includes(MediaType.TEXT_XML) || + acceptMediaType.includes(MediaType.APPLICATION_XML)) { + break; + } else if (acceptMediaType.includes(MediaType.TEXT_HTML)) { + shouldServeAsHTML = true; + } + } + } + + if (shouldServeAsHTML) { + response.setContentType("text/html"); + out = new ByteArrayOutputStream(); + } else { + response.setContentType("text/xml"); + } response.setCharacterEncoding("UTF-8"); String identification = xoaiContext + parameters.requestID(); @@ -124,6 +185,15 @@ public String contextAction(Model model, HttpServletRequest request, HttpServlet dataProvider.handle(parameters, out); } + if (shouldServeAsHTML) { + OutputStream responseOut = response.getOutputStream(); + Source source = new StreamSource(new ByteArrayInputStream(((ByteArrayOutputStream) out).toByteArray())); + Result result = new StreamResult(responseOut); + Transformer htmlTransformer = htmlTransformerFactory.newTransformer( + new StreamSource(new ByteArrayInputStream(htmlTransformerSource))); + htmlTransformer.transform(source, result); + out = responseOut; + } out.flush(); out.close(); diff --git a/dspace-oai/src/main/java/org/dspace/xoai/services/api/config/ConfigurationService.java b/dspace-oai/src/main/java/org/dspace/xoai/services/api/config/ConfigurationService.java index bc6083166baf..997e08adec83 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/services/api/config/ConfigurationService.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/services/api/config/ConfigurationService.java @@ -15,4 +15,6 @@ public interface ConfigurationService { boolean getBooleanProperty(String module, String key, boolean defaultValue); boolean getBooleanProperty(String key, boolean defaultValue); + + void ensureRequiredConfiguration(); } diff --git a/dspace-oai/src/main/java/org/dspace/xoai/services/impl/config/DSpaceConfigurationService.java b/dspace-oai/src/main/java/org/dspace/xoai/services/impl/config/DSpaceConfigurationService.java index 67d4f09f5c88..0fb0c51defd3 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/services/impl/config/DSpaceConfigurationService.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/services/impl/config/DSpaceConfigurationService.java @@ -20,12 +20,18 @@ public class DSpaceConfigurationService implements ConfigurationService { * Initialize the OAI Configuration Service */ public DSpaceConfigurationService() { - // Check the DSpace ConfigurationService for required OAI-PMH settings. - // If they do not exist, set sane defaults as needed. + ensureRequiredConfiguration(); + } - // Per OAI Spec, "oai.identifier.prefix" should be the hostname / domain name of the site. - // This configuration is needed by the [dspace]/config/crosswalks/oai/description.xml template, so if - // unspecified we will dynamically set it to the hostname of the "dspace.ui.url" configuration. + /** + * Check the DSpace ConfigurationService for required OAI-PMH settings. + * If they do not exist, set sane defaults as needed. + *
+ * Per OAI Spec, "oai.identifier.prefix" should be the hostname / domain name of the site. + * This configuration is needed by the [dspace]/config/crosswalks/oai/description.xml template, so if + * unspecified we will dynamically set it to the hostname of the "dspace.ui.url" configuration. + */ + public void ensureRequiredConfiguration() { if (!configurationService.hasProperty("oai.identifier.prefix")) { configurationService.setProperty("oai.identifier.prefix", Utils.getHostName(configurationService.getProperty("dspace.ui.url"))); diff --git a/dspace-oai/src/main/java/org/dspace/xoai/services/impl/xoai/DSpaceRepositoryConfiguration.java b/dspace-oai/src/main/java/org/dspace/xoai/services/impl/xoai/DSpaceRepositoryConfiguration.java index 8c9841dfb949..38b354f05919 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/services/impl/xoai/DSpaceRepositoryConfiguration.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/services/impl/xoai/DSpaceRepositoryConfiguration.java @@ -133,6 +133,7 @@ public String getRepositoryName() { @Override public List getDescription() { + configurationService.ensureRequiredConfiguration(); List result = new ArrayList(); String descriptionFile = configurationService.getProperty("oai.description.file"); if (descriptionFile == null) { 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 40a193ea2905..78c98533e0b4 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 @@ -18,6 +18,7 @@ import com.lyncode.xoai.dataprovider.xml.xoai.Element; import com.lyncode.xoai.dataprovider.xml.xoai.Metadata; import com.lyncode.xoai.util.Base64Utils; +import org.apache.commons.text.StringEscapeUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.app.util.factory.UtilServiceFactory; @@ -143,7 +144,7 @@ private static Element createBundlesElement(Context context, Item item) throws S bitstream.getField().add(createValue("name", name)); } if (oname != null) { - bitstream.getField().add(createValue("originalName", name)); + bitstream.getField().add(createValue("originalName", oname)); } if (description != null) { bitstream.getField().add(createValue("description", description)); @@ -165,6 +166,19 @@ private static Element createBundlesElement(Context context, Item item) throws S return bundles; } + /** + * Sanitizes a string to remove characters that are invalid + * in XML 1.0 using the Apache Commons Text library. + * @param value The string to sanitize. + * @return A sanitized string, or null if the input was null. + */ + private static String sanitize(String value) { + if (value == null) { + return null; + } + return StringEscapeUtils.escapeXml10(value); + } + /** * This method will add metadata information about associated resource policies for a give bitstream. * It will parse of relevant policies and add metadata information @@ -281,7 +295,7 @@ private static void fillSchemaElement(Element schema, MetadataValue val) throws valueElem = language; } - valueElem.getField().add(createValue("value", val.getValue())); + valueElem.getField().add(createValue("value", sanitize(val.getValue()))); if (val.getAuthority() != null) { valueElem.getField().add(createValue("authority", val.getAuthority())); if (val.getConfidence() != Choices.CF_NOVALUE) { diff --git a/dspace-rdf/pom.xml b/dspace-rdf/pom.xml index f9ffa5f2a15e..87531e96e3c1 100644 --- a/dspace-rdf/pom.xml +++ b/dspace-rdf/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT .. @@ -67,11 +67,6 @@ org.springframework.boot spring-boot-starter-logging - - - org.springframework - spring-jcl - diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 96e96006d6e6..fc48442a3ad1 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -14,7 +14,7 @@ org.dspace dspace-parent - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT .. @@ -31,7 +31,7 @@ org.codehaus.mojo properties-maven-plugin - 1.2.1 + 1.3.0 initialize @@ -460,11 +460,6 @@ org.springframework.boot spring-boot-starter-logging - - - org.springframework - spring-jcl - @@ -481,6 +476,13 @@ + + + + org.dspace + dspace-iiif + + org.dspace dspace-api @@ -500,10 +502,6 @@ - - org.dspace - dspace-iiif - org.dspace dspace-oai @@ -552,7 +550,7 @@ net.minidev json-smart - 2.5.2 + 2.6.0 @@ -590,7 +588,7 @@ org.apache.httpcomponents.client5 httpclient5 - 5.5 + 5.6.1 test 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 11b048e23ef1..76dca5128032 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 @@ -20,7 +20,6 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.ws.rs.core.Response; import org.apache.catalina.connector.ClientAbortException; -import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.app.rest.converter.ConverterService; @@ -110,7 +109,7 @@ public ResponseEntity retrieve(@PathVariable UUID uuid, HttpServletResponse resp Bitstream bit = bitstreamService.find(context, uuid); EPerson currentUser = context.getCurrentUser(); - if (bit == null) { + if (bit == null || bit.isDeleted()) { response.sendError(HttpServletResponse.SC_NOT_FOUND); return null; } @@ -153,12 +152,16 @@ public ResponseEntity retrieve(@PathVariable UUID uuid, HttpServletResponse resp httpHeadersInitializer.withLastModified(lastModified); } - //Determine if we need to send the file as a download or if the browser can open it inline - //The file will be downloaded if its size is larger than the configured threshold, - //or if its mimetype/extension appears in the "webui.content_disposition_format" config - long dispositionThreshold = configurationService.getLongProperty("webui.content_disposition_threshold"); - if ((dispositionThreshold >= 0 && filesize > dispositionThreshold) - || checkFormatForContentDisposition(format)) { + // Determine if we need to send the file as a download or if the browser can open it inline. + // By default, all files will be downloaded as that is more secure. File formats will only be opened inline + // if they are listed in the "webui.content_disposition_inline" config and size is less than the + // configured "webui.content_disposition_threshold" (default = 8MB) + long dispositionThreshold = configurationService.getLongProperty("webui.content_disposition_threshold", + 8388608); + if (checkFormatForContentDispositionInline(format) && + filesize <= dispositionThreshold) { + httpHeadersInitializer.withDisposition(HttpHeadersInitializer.CONTENT_DISPOSITION_INLINE); + } else { httpHeadersInitializer.withDisposition(HttpHeadersInitializer.CONTENT_DISPOSITION_ATTACHMENT); } @@ -206,48 +209,48 @@ private boolean isNotAnErrorResponse(HttpServletResponse response) { } /** - * Check if a Bitstream of the specified format should always be downloaded (i.e. "content-disposition: attachment") - * or can be opened inline (i.e. "content-disposition: inline"). + * Check if a Bitstream of the specified format should be opened inline (i.e. "content-disposition: inline") + * instead of the default behavior of always downloading (i.e. "content-disposition: attachment"). *

* NOTE that downloading via "attachment" is more secure, as the user's browser will not attempt to process or * display the file. But, downloading via "inline" may be seen as more user-friendly for common formats. * @param format BitstreamFormat - * @return true if always download ("attachment"). false if can be opened inline ("inline") + * @return true if format is configured to be opened inline ("inline"). false if always download ("attachment") */ - private boolean checkFormatForContentDisposition(BitstreamFormat format) { + private boolean checkFormatForContentDispositionInline(BitstreamFormat format) { // Undefined or Unknown formats should ALWAYS be downloaded for additional security. if (format == null || format.getSupportLevel() == BitstreamFormat.UNKNOWN) { - return true; + return false; } - // Load additional formats configured to require download - List configuredFormats = List.of(configurationService. - getArrayProperty("webui.content_disposition_format")); - - // If configuration includes "*", then all formats will always be downloaded. - if (configuredFormats.contains("*")) { - return true; + // Return false for BANNED inline formats. Some formats, especially XML / HTML / Javascript based formats, + // when loaded inline may be susceptible to XSS attacks. Therefore, we will refuse to allow those formats to be + // displayed inline for security purposes. + // NOTE: "+xml" in this list will match any MIME Type that ends in "+xml", as those are XML-based formats. + List bannedInlineFormats = List.of("text/html", "text/javascript", "text/xml", "application/xml", + "+xml"); + for (String bannedInlineFormat : bannedInlineFormats) { + // If our format MIME Type contains one of the banned inline formats, we refuse to display it inline + if (format.getMIMEType().contains(bannedInlineFormat)) { + return false; + } } - // Define a download list of formats which DSpace forces to ALWAYS be downloaded. - // These formats can embed JavaScript which may be run in the user's browser if the file is opened inline. - // Therefore, DSpace blocks opening these formats inline as it could be used for an XSS attack. - List downloadOnlyFormats = List.of("text/html", "text/javascript", "text/xml", "rdf"); - - // Combine our two lists - List formats = ListUtils.union(downloadOnlyFormats, configuredFormats); + // Load formats configured to allow inline display + List formats = List.of(configurationService. + getArrayProperty("webui.content_disposition_inline")); // See if the passed in format's MIME type or file extension is listed. - boolean download = formats.contains(format.getMIMEType()); - if (!download) { + boolean inline = formats.contains(format.getMIMEType()); + if (!inline) { for (String ext : format.getExtensions()) { if (formats.contains(ext)) { - download = true; + inline = true; break; } } } - return download; + return inline; } /** diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/LDNInboxController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/LDNInboxController.java index f0ccbcf873c4..cff51520b3df 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/LDNInboxController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/LDNInboxController.java @@ -120,7 +120,7 @@ private void validate(Context context, Notification notification, String sourceI } } catch (SQLException sqle) { throw new DSpaceBadRequestException("Notify Service [" + notification.getOrigin() - + "] unknown. LDN message can not be received."); + + "] unknown. LDN message can not be received."); } } if (configurationService.getBooleanProperty("ldn.notify.inbox.block-untrusted-ip", true)) { @@ -138,7 +138,7 @@ private void validate(Context context, Notification notification, String sourceI } } catch (SQLException sqle) { throw new DSpaceBadRequestException("Notify Service [" + notification.getOrigin() - + "] unknown. LDN message can not be received."); + + "] unknown. LDN message can not be received."); } } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyEPersonReplaceRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyEPersonReplaceRestController.java index 3311f303ade6..b8b638c82ae4 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyEPersonReplaceRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyEPersonReplaceRestController.java @@ -75,6 +75,7 @@ public ResponseEntity> replaceEPersonOfResourcePolicy(@Pa } EPerson newEPerson = (EPerson) dsoList.get(0); resourcePolicy.setEPerson(newEPerson); + resourcePolicyService.update(context, resourcePolicy); context.commit(); return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyGroupReplaceRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyGroupReplaceRestController.java index 2a041aba3a0a..6f08cf2cf99a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyGroupReplaceRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyGroupReplaceRestController.java @@ -75,6 +75,7 @@ public ResponseEntity> replaceGroupOfResourcePolicy(@Path Group newGroup = (Group) dsoList.get(0); resourcePolicy.setGroup(newGroup); + resourcePolicyService.update(context, resourcePolicy); context.commit(); return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/WebApplication.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/WebApplication.java index 4c835d99183c..5494475b5241 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/WebApplication.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/WebApplication.java @@ -9,8 +9,11 @@ import java.io.IOException; import java.sql.SQLException; +import java.time.ZoneOffset; import java.util.List; +import java.util.TimeZone; +import jakarta.annotation.PostConstruct; import jakarta.servlet.Filter; import org.dspace.app.ldn.LDNQueueExtractor; import org.dspace.app.ldn.LDNQueueTimeoutChecker; @@ -246,4 +249,12 @@ public void addArgumentResolvers(@NonNull List ar } }; } + + @PostConstruct + public void setDefaultTimezone() { + // Set the default timezone in Spring Boot to UTC. + // This ensures that Spring Boot doesn't attempt to change the timezone of dates that are read from the + // database (via Hibernate). We store all dates in the database as UTC. + TimeZone.setDefault(TimeZone.getTimeZone(ZoneOffset.UTC)); + } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EditItemFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EditItemFeature.java index 5c605daaf407..d938cd5a335e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EditItemFeature.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EditItemFeature.java @@ -40,7 +40,7 @@ public class EditItemFeature implements AuthorizationFeature { @Override public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException, SearchServiceException { if (object instanceof SiteRest) { - return itemService.countItemsWithEdit(context) > 0; + return itemService.countItemsWithEdit(context, "") > 0; } else if (object instanceof ItemRest) { Item item = (Item) utils.getDSpaceAPIObjectFromRest(context, object); return authService.authorizeActionBoolean(context, item, Constants.WRITE); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/SubmitFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/SubmitFeature.java index 3793928fb0fe..599bdb64117f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/SubmitFeature.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/SubmitFeature.java @@ -43,7 +43,7 @@ public class SubmitFeature implements AuthorizationFeature { public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException, SearchServiceException { if (object instanceof SiteRest) { // Check whether the user has permission to add to any collection - return collectionService.countCollectionsWithSubmit("", context, null) > 0; + return collectionService.countCollectionsWithSubmit(context, "", null) > 0; } else if (object instanceof CollectionRest) { // Check whether the user has permission to add to the given collection Collection collection = (Collection) utils.getDSpaceAPIObjectFromRest(context, object); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionDefinitionConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionDefinitionConverter.java index 8e4fd247874c..55c385b27155 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionDefinitionConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionDefinitionConverter.java @@ -7,24 +7,16 @@ */ package org.dspace.app.rest.converter; -import java.sql.SQLException; import java.util.LinkedList; import java.util.List; -import java.util.stream.Collectors; -import jakarta.servlet.http.HttpServletRequest; import org.apache.logging.log4j.Logger; -import org.dspace.app.rest.model.CollectionRest; import org.dspace.app.rest.model.SubmissionDefinitionRest; import org.dspace.app.rest.model.SubmissionSectionRest; import org.dspace.app.rest.projection.Projection; import org.dspace.app.rest.submit.DataProcessingStep; -import org.dspace.app.rest.utils.ContextUtil; import org.dspace.app.util.SubmissionConfig; -import org.dspace.app.util.SubmissionConfigReaderException; import org.dspace.app.util.SubmissionStepConfig; -import org.dspace.content.Collection; -import org.dspace.core.Context; import org.dspace.services.RequestService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; @@ -75,21 +67,6 @@ public SubmissionDefinitionRest convert(SubmissionConfig obj, Projection project e); } } - - HttpServletRequest request = requestService.getCurrentRequest().getHttpServletRequest(); - Context context = null; - try { - context = ContextUtil.obtainContext(request); - List collections = panelConverter.getSubmissionConfigService() - .getCollectionsBySubmissionConfig(context, - obj.getSubmissionName()); - DSpaceConverter cc = converter.getConverter(Collection.class); - List collectionsRest = collections.stream().map((collection) -> - cc.convert(collection, projection)).collect(Collectors.toList()); - sd.setCollections(collectionsRest); - } catch (SQLException | IllegalStateException | SubmissionConfigReaderException e) { - log.error(e.getMessage(), e); - } sd.setPanels(panels); return sd; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/CollectionRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/CollectionRestRepository.java index 0d59aeb254b5..5e28bdb30645 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/CollectionRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/CollectionRestRepository.java @@ -185,7 +185,7 @@ public Page findSubmitAuthorizedByCommunity( List collections = cs.findCollectionsWithSubmit(q, context, com, Math.toIntExact(pageable.getOffset()), Math.toIntExact(pageable.getPageSize())); - int tot = cs.countCollectionsWithSubmit(q, context, com); + int tot = cs.countCollectionsWithSubmit(context, q, com); return converter.toRestPage(collections, pageable, tot , utils.obtainProjection()); } catch (SQLException | SearchServiceException e) { throw new RuntimeException(e.getMessage(), e); @@ -200,7 +200,7 @@ public Page findSubmitAuthorized(@Parameter(value = "query") Str List collections = cs.findCollectionsWithSubmit(q, context, null, Math.toIntExact(pageable.getOffset()), Math.toIntExact(pageable.getPageSize())); - int tot = cs.countCollectionsWithSubmit(q, context, null); + int tot = cs.countCollectionsWithSubmit(context, q, null); return converter.toRestPage(collections, pageable, tot, utils.obtainProjection()); } catch (SQLException e) { throw new RuntimeException(e.getMessage(), e); @@ -211,14 +211,33 @@ public Page findSubmitAuthorized(@Parameter(value = "query") Str @SearchRestMethod(name = "findAdminAuthorized") public Page findAdminAuthorized ( Pageable pageable, @Parameter(value = "query") String query) { + return findAuthorized(pageable, Constants.ADMIN, query); + } + + /** + * Returns Collections for which the current user has 'edit' privileges. + * + * @param pageable The pagination information + * @param query The query used in the lookup + * @return + */ + @PreAuthorize("hasAuthority('AUTHENTICATED')") + @SearchRestMethod(name = "findEditAuthorized") + public Page findEditAuthorized ( + Pageable pageable, @Parameter(value = "query") String query) { + return findAuthorized(pageable, Constants.WRITE, query); + } + + private Page findAuthorized(Pageable pageable, int action, String query) { try { Context context = obtainContext(); - List collections = authorizeService.findAdminAuthorizedCollection(context, query, + List collections = authorizeService.findAuthorizedCollectionByAction(context, query, + action, Math.toIntExact(pageable.getOffset()), Math.toIntExact(pageable.getPageSize())); - long tot = authorizeService.countAdminAuthorizedCollection(context, query); - return converter.toRestPage(collections, pageable, tot , utils.obtainProjection()); - } catch (SearchServiceException | SQLException e) { + long tot = authorizeService.countAuthorizedCollectionByAction(context, query, action); + return converter.toRestPage(collections, pageable, tot, utils.obtainProjection()); + } catch (SearchServiceException e) { throw new RuntimeException(e.getMessage(), e); } } @@ -245,10 +264,10 @@ public Page findSubmitAuthorizedByEntityType( if (entityType == null) { throw new ResourceNotFoundException("There was no entityType found with label: " + entityTypeLabel); } - List collections = cs.findCollectionsWithSubmit(query, context, null, entityTypeLabel, + List collections = cs.findCollectionsWithSubmit(context, query,null, entityTypeLabel, Math.toIntExact(pageable.getOffset()), Math.toIntExact(pageable.getPageSize())); - int tot = cs.countCollectionsWithSubmit(query, context, null, entityTypeLabel); + int tot = cs.countCollectionsWithSubmit(context, query,null, entityTypeLabel); return converter.toRestPage(collections, pageable, tot, utils.obtainProjection()); } catch (SQLException e) { throw new RuntimeException(e.getMessage(), e); @@ -282,10 +301,10 @@ public Page findSubmitAuthorizedByCommunityAndEntityType( throw new ResourceNotFoundException( CommunityRest.CATEGORY + "." + CommunityRest.NAME + " with id: " + communityUuid + " not found"); } - List collections = cs.findCollectionsWithSubmit(query, context, community, entityTypeLabel, + List collections = cs.findCollectionsWithSubmit(context, query, community, entityTypeLabel, Math.toIntExact(pageable.getOffset()), Math.toIntExact(pageable.getPageSize())); - int total = cs.countCollectionsWithSubmit(query, context, community, entityTypeLabel); + int total = cs.countCollectionsWithSubmit(context, query, community, entityTypeLabel); return converter.toRestPage(collections, pageable, total, utils.obtainProjection()); } catch (SQLException | SearchServiceException e) { throw new RuntimeException(e.getMessage(), e); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/CommunityRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/CommunityRestRepository.java index 0d4e6be133e8..9bbdd3c75d6e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/CommunityRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/CommunityRestRepository.java @@ -38,6 +38,7 @@ import org.dspace.content.Community; import org.dspace.content.service.BitstreamService; import org.dspace.content.service.CommunityService; +import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.discovery.DiscoverQuery; import org.dspace.discovery.DiscoverResult; @@ -220,12 +221,31 @@ public Page findAllTop(Pageable pageable) { @SearchRestMethod(name = "findAdminAuthorized") public Page findAdminAuthorized ( Pageable pageable, @Parameter(value = "query") String query) { + return findAuthorized(pageable, Constants.ADMIN, query); + } + + @PreAuthorize("hasAuthority('AUTHENTICATED')") + @SearchRestMethod(name = "findEditAuthorized") + public Page findEditAuthorized ( + Pageable pageable, @Parameter(value = "query") String query) { + return findAuthorized(pageable, Constants.WRITE, query); + } + + @PreAuthorize("hasAuthority('AUTHENTICATED')") + @SearchRestMethod(name = "findAddAuthorized") + public Page findAddAuthorized ( + Pageable pageable, @Parameter(value = "query") String query) { + return findAuthorized(pageable, Constants.ADD, query); + } + + private Page findAuthorized(Pageable pageable, int action, String query) { try { Context context = obtainContext(); - List communities = authorizeService.findAdminAuthorizedCommunity(context, query, + List communities = authorizeService.findAuthorizedCommunityByAction(context, query, + action, Math.toIntExact(pageable.getOffset()), Math.toIntExact(pageable.getPageSize())); - long tot = authorizeService.countAdminAuthorizedCommunity(context, query); + long tot = authorizeService.countAuthorizedCommunityByAction(context, query, action); return converter.toRestPage(communities, pageable, tot , utils.obtainProjection()); } catch (SearchServiceException | SQLException e) { throw new RuntimeException(e.getMessage(), e); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemRestRepository.java index 1a22a5f7477f..c4a800681bba 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemRestRepository.java @@ -22,6 +22,8 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.rest.Parameter; +import org.dspace.app.rest.SearchRestMethod; import org.dspace.app.rest.converter.MetadataConverter; import org.dspace.app.rest.exception.DSpaceBadRequestException; import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException; @@ -45,6 +47,7 @@ import org.dspace.content.service.RelationshipTypeService; import org.dspace.content.service.WorkspaceItemService; import org.dspace.core.Context; +import org.dspace.discovery.SearchServiceException; import org.dspace.util.UUIDUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -357,6 +360,27 @@ public Bundle addBundleToItem(Context context, Item item, BundleRest bundleRest) return bundle; } + /** + * Method to find the items for which the current user has editing rights. + * + * @param query Query string + * @param pageable Pagination information + * @return Page of Items (REST representation) for which the current user has editing rights + * @throws SearchServiceException + */ + @PreAuthorize("hasAuthority('AUTHENTICATED')") + @SearchRestMethod(name = "findEditAuthorized") + public Page findEditAuthorized(@Parameter(value = "query") String query, + Pageable pageable) + throws SearchServiceException { + Context context = obtainContext(); + List items = itemService.findItemsWithEdit(context, query, + Math.toIntExact(pageable.getOffset()), + Math.toIntExact(pageable.getPageSize())); + int tot = itemService.countItemsWithEdit(context, query); + return converter.toRestPage(items, pageable, tot, utils.obtainProjection()); + } + @Override protected ItemRest createAndReturn(Context context, List stringList) throws AuthorizeException, SQLException, RepositoryMethodNotImplementedException { 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 6ec7c62aa679..9942f55b82a5 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,7 +14,6 @@ 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; @@ -38,6 +37,8 @@ import org.dspace.scripts.Process_; import org.dspace.scripts.service.ProcessService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -66,7 +67,10 @@ public class ProcessRestRepository extends DSpaceRestRepository= 0) { + url = url.substring(0, semicolon); + } + url = decodeUrl(url); + if (url == null || url.isBlank()) { + throw new IOException("Decoded URL path is empty"); + } + url = normaliseUrlPath(url); + if (url == null || url.isBlank()) { + throw new IOException("Normalised URL path is empty"); + } + return url.toLowerCase(Locale.ROOT); + } + + /** + * Decode URL, falling back to original URL if it's malformed or undecodable + * @param url the encoded / unvalidated URL + * @return decoded URL or the original URL on error + */ + private String decodeUrl(String url) { + try { + return URLDecoder.decode(url, StandardCharsets.UTF_8); + } catch (IllegalArgumentException ex) { + // if we can't decode it, just return raw string + return url; + } + } + + /** + * Normalise the URL path and ensure it ends in a / + * @param url the URL path to normalise + * @return normalised path or the original parameter on error + */ + private String normaliseUrlPath(String url) { + try { + if (!url.startsWith("/")) { + url = "/" + url; + } + return new URI(url).normalize().getPath(); + } catch (Exception e) { + // if we can't use or normalise the path, just return the raw string + return url; + } + } + + /** + * Detect traversal after normalisation + * @param url the URL path to validate + * @return true if this looks like a traversal attempt + */ + private boolean isTraversalAttempt(String url) { + return url.contains("../") + || url.contains("/..") + || url.contains("%2e%2e") + || url.contains(".."); + } + + /** + * Block JSP execution attempts + * @param url the URL path to validate + */ + private boolean isJspExecutionAttempt(String url) { + return url.endsWith(".jsp") + || url.endsWith(".jspx") + || url.contains(".jsp/") + || url.contains(".jspx/") + || url.contains(".jsp\0") + || url.contains(".jspx\0"); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidLoginFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidLoginFilter.java index 70496b9dba23..63ba9a2de0eb 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidLoginFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidLoginFilter.java @@ -89,6 +89,7 @@ protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServle String baseRediredirectUrl = configurationService.getProperty("dspace.ui.url"); String redirectUrl = baseRediredirectUrl + "/error?status=401&code=orcid.generic-error"; response.sendRedirect(redirectUrl); // lgtm [java/unvalidated-url-redirection] + this.closeOpenContext(request); } else { super.unsuccessfulAuthentication(request, response, failed); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java index cfae6bfcb42b..70d731f0b7b2 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java @@ -8,6 +8,7 @@ package org.dspace.app.rest.security; import java.io.IOException; +import java.sql.SQLException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -15,11 +16,14 @@ import jakarta.servlet.http.HttpServletResponse; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.core.Context; +import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; /** * This class will filter /api/authn/login requests to try and authenticate them. Keep in mind, this filter runs *after* @@ -54,7 +58,7 @@ public void afterPropertiesSet() { public StatelessLoginFilter(String url, String httpMethod, AuthenticationManager authenticationManager, RestAuthenticationService restAuthenticationService) { // NOTE: attemptAuthentication() below will only be triggered by requests that match both this URL and method - super(new AntPathRequestMatcher(url, httpMethod)); + super(PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.valueOf(httpMethod),url)); this.authenticationManager = authenticationManager; this.restAuthenticationService = restAuthenticationService; } @@ -133,6 +137,27 @@ protected void unsuccessfulAuthentication(HttpServletRequest request, response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication failed!"); log.error("Authentication failed (status:{})", HttpServletResponse.SC_UNAUTHORIZED, failed); + this.closeOpenContext(request); + } + + /** + * Manually closes the open {@link Context} if one exists. We need to do this manually because + * {@link #continueChainBeforeSuccessfulAuthentication} is {@code false} by default, which prevents the + * {@link org.dspace.app.rest.filter.DSpaceRequestContextFilter} from being called. Without this call, the request + * would leave an open database connection. + * + * @param request The current request. + */ + protected void closeOpenContext(HttpServletRequest request) { + if (ContextUtil.isContextAvailable(request)) { + try (Context context = ContextUtil.obtainContext(request)) { + if (context != null && context.isValid()) { + context.complete(); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java index 333263c088ac..00299737e334 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java @@ -30,7 +30,6 @@ import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.security.web.csrf.CsrfTokenRepository; import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; /** * Spring Security configuration for DSpace Server Webapp @@ -88,12 +87,23 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // Get the current AuthenticationManager (defined above) to apply filters below AuthenticationManager authenticationManager = authenticationManager(); + // Create a custom CsrfTokenRequestHandler to restore the eager loading of the CSRF token. + // In DSpace 8+, the upgrade to Spring Security 6 changed the default behavior to "deferred loading", + // which meant the DSPACE-XSRF-TOKEN was no longer automatically sent on most GET requests. + // This was a breaking change for REST API clients expecting the DSpace 7.x behavior. + // + // By setting the csrfRequestAttributeName to null, we explicitly opt-out of deferred loading and + // force Spring Security to load the token on every request, restoring the old functionality. + // This resolves https://github.com/DSpace/DSpace/issues/9774 + CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler(); + requestHandler.setCsrfRequestAttributeName(null); + // Configure authentication requirements for ${dspace.server.url}/api/ URL only // NOTE: REST API is hardcoded to respond on /api/. Other modules (OAI, SWORD, IIIF, etc) use other root paths. http.securityMatcher("/api/**", "/iiif/**", actuatorBasePath + "/**", "/signposting/**") .authorizeHttpRequests((requests) -> requests // Ensure /actuator/info endpoint is restricted to admins - .requestMatchers(new AntPathRequestMatcher(actuatorBasePath + "/info", HttpMethod.GET.name())) + .requestMatchers(HttpMethod.GET, actuatorBasePath + "/info") .hasAnyAuthority(ADMIN_GRANT) // All other requests should be permitted at this layer because we check permissions on each method // via @PreAuthorize annotations. As this code runs first, we must permitAll() here in order to pass @@ -118,7 +128,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // See https://github.com/DSpace/DSpace/issues/9450 // NOTE: DSpace doesn't need BREACH protection as it's only necessary when sending the token via a // request attribute (e.g. "_csrf") which the DSpace UI never does. - .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())) + .csrfTokenRequestHandler(requestHandler)) .exceptionHandling((exceptionHandling) -> exceptionHandling // Return 401 on authorization failures with a correct WWWW-Authenticate header .authenticationEntryPoint(new DSpace401AuthenticationEntryPoint(restAuthenticationService)) @@ -130,7 +140,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // On logout, clear the "session" salt .addLogoutHandler(customLogoutHandler) // Configure the logout entry point & require POST - .logoutRequestMatcher(new AntPathRequestMatcher("/api/authn/logout", HttpMethod.POST.name())) + // If CSRF protection is enabled (default in DSpace REST), a POST request is needed to trigger logout + .logoutUrl("/api/authn/logout") // When logout is successful, return OK (204) status .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.NO_CONTENT)) ) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java index 727267744fb1..f3161e453777 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java @@ -65,7 +65,7 @@ public abstract class JWTTokenHandler { private List jwtClaimProviders; @Autowired - private ConfigurationService configurationService; + protected ConfigurationService configurationService; @Autowired private EPersonClaimProvider ePersonClaimProvider; @@ -79,6 +79,13 @@ public abstract class JWTTokenHandler { private String generatedJwtKey; private String generatedEncryptionKey; + /** + * Get the default expiration period for this handler if not + * defined in configuration. + * @return default expiration period if not explicitly defined in configuration + */ + public abstract long getExpirationPeriod(); + /** * Get the configuration property key for the token secret. * @return the configuration property key @@ -220,10 +227,6 @@ public String getJwtKey() { return secret; } - public long getExpirationPeriod() { - return configurationService.getLongProperty(getTokenExpirationConfigurationKey(), 1800000); - } - public boolean isEncryptionEnabled() { return configurationService.getBooleanProperty(getEncryptionEnabledConfigurationKey(), false); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/LoginJWTTokenHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/LoginJWTTokenHandler.java index 1fad84165809..46877b03c3ca 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/LoginJWTTokenHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/LoginJWTTokenHandler.java @@ -15,6 +15,17 @@ */ @Component public class LoginJWTTokenHandler extends JWTTokenHandler { + + /** + * Default expiration period for login tokens in milliseconds + */ + private static final long DEFAULT_EXPIRATION_PERIOD = 1800000; + + @Override + public long getExpirationPeriod() { + return configurationService.getLongProperty(getTokenExpirationConfigurationKey(), DEFAULT_EXPIRATION_PERIOD); + } + @Override protected String getTokenSecretConfigurationKey() { return "jwt.login.token.secret"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java index ac7a73a796ef..94bf1f5b1d80 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java @@ -28,6 +28,11 @@ @Component public class ShortLivedJWTTokenHandler extends JWTTokenHandler { + /** + * Default expiration period for short-lived tokens in milliseconds + */ + private static final long DEFAULT_EXPIRATION_PERIOD = 2000; + /** * Determine if current JWT is valid for the given EPerson object. * To be valid, current JWT *must* have been signed by the EPerson and not be expired. @@ -67,6 +72,11 @@ protected EPerson updateSessionSalt(final Context context, final Date previousLo return context.getCurrentUser(); } + @Override + public long getExpirationPeriod() { + return configurationService.getLongProperty(getTokenExpirationConfigurationKey(), DEFAULT_EXPIRATION_PERIOD); + } + @Override protected String getTokenSecretConfigurationKey() { return "jwt.shortLived.token.secret"; 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 42b1c8184957..cd5d1a62b798 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 @@ -18,6 +18,7 @@ import org.dspace.app.rest.security.BitstreamMetadataReadPermissionEvaluatorPlugin; import org.dspace.app.rest.signposting.model.LinksetNode; import org.dspace.app.rest.signposting.processor.bitstream.BitstreamSignpostingProcessor; +import org.dspace.app.rest.signposting.processor.item.ItemLinksetProcessor; import org.dspace.app.rest.signposting.processor.item.ItemSignpostingProcessor; import org.dspace.app.rest.signposting.processor.metadata.MetadataSignpostingProcessor; import org.dspace.app.rest.signposting.service.LinksetService; @@ -25,9 +26,11 @@ import org.dspace.content.Bundle; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; +import org.dspace.content.service.BundleService; import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.core.Context; +import org.dspace.services.ConfigurationService; import org.dspace.utils.DSpace; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -40,12 +43,21 @@ public class LinksetServiceImpl implements LinksetService { private static final Logger log = LogManager.getLogger(LinksetServiceImpl.class); + @Autowired + private ConfigurationService configurationService; + @Autowired protected ItemService itemService; + @Autowired + protected BundleService bundleService; + @Autowired private BitstreamMetadataReadPermissionEvaluatorPlugin bitstreamMetadataReadPermissionEvaluatorPlugin; + @Autowired + ItemLinksetProcessor itemLinksetProcessor; + private final List bitstreamProcessors = new DSpace().getServiceManager() .getServicesByType(BitstreamSignpostingProcessor.class); @@ -74,10 +86,20 @@ public List createLinksetNodesForSingleLinkset( Context context, DSpaceObject object ) { + int itemBitstreamsLimit = configurationService.getIntProperty("signposting.item.bitstreams.limit", 10); + List linksetNodes = new ArrayList<>(); if (object.getType() == Constants.ITEM) { - for (ItemSignpostingProcessor processor : itemProcessors) { - processor.addLinkSetNodes(context, request, (Item) object, linksetNodes); + int itemBitstreamsCount = countItemBitstreams(context, (Item) object); + + // Do not include individual bitstream typed links if their number exceeds + // the limit in the configuration. + if (itemBitstreamsCount < itemBitstreamsLimit) { + for (ItemSignpostingProcessor processor : itemProcessors) { + processor.addLinkSetNodes(context, request, (Item) object, linksetNodes); + } + } else { + itemLinksetProcessor.addLinkSetNodes(context, request, (Item) object, linksetNodes); } } else if (object.getType() == Constants.BITSTREAM) { for (BitstreamSignpostingProcessor processor : bitstreamProcessors) { @@ -151,4 +173,17 @@ private Iterator getItemBitstreams(Context context, Item item) { throw new RuntimeException(e); } } + + private int countItemBitstreams(Context context, Item item) { + try { + int countBitstreams = 0; + List bundles = itemService.getBundles(item, Constants.DEFAULT_BUNDLE_NAME); + for (Bundle bundle: bundles) { + countBitstreams += bundleService.countBitstreams(context, bundle); + } + return countBitstreams; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/MetadataValueRemovePatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/MetadataValueRemovePatchOperation.java index 1660a5455aea..18bc1df66c1c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/MetadataValueRemovePatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/MetadataValueRemovePatchOperation.java @@ -11,6 +11,8 @@ import java.util.Arrays; import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.MetadataValueRest; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; @@ -27,6 +29,8 @@ public abstract class MetadataValueRemovePatchOperation extends RemovePatchOperation { + private static final Logger log = LogManager.getLogger(); + @Override protected Class getArrayClassForEvaluation() { return MetadataValueRest[].class; @@ -42,7 +46,12 @@ protected void deleteValue(Context context, DSO source, String target, int index List mm = getDSpaceObjectService().getMetadata(source, metadata[0], metadata[1], metadata[2], Item.ANY); if (index != -1) { - getDSpaceObjectService().removeMetadataValues(context, source, Arrays.asList(mm.get(index))); + if (index < mm.size()) { + getDSpaceObjectService().removeMetadataValues(context, source, Arrays.asList(mm.get(index))); + } else { + log.warn("value of index ({}) is out of range of the metadata value list of size {} (target: {})", + index, mm.size(), target); + } } else { getDSpaceObjectService().clearMetadata(context, source, metadata[0], metadata[1], metadata[2], Item.ANY); } 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 1ac6a320d9c2..3d10d9d02c10 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 @@ -28,6 +28,7 @@ import org.dspace.eperson.service.EPersonService; import org.dspace.utils.DSpace; import org.springframework.core.io.AbstractResource; +import org.springframework.core.io.InputStreamSource; import org.springframework.util.DigestUtils; /** @@ -92,7 +93,7 @@ public String getDescription() { public InputStream getInputStream() throws IOException { fetchDocument(); - return document.inputStream(); + return document.getInputStream(); } @Override @@ -110,7 +111,7 @@ public long contentLength() { public String getChecksum() { fetchDocument(); - return document.etag(); + return document.getEtag(); } private void fetchDocument() { @@ -123,13 +124,13 @@ private void fetchDocument() { if (shouldGenerateCoverPage) { var coverPage = getCoverpageByteArray(context, bitstream); - this.document = new BitstreamDocument(etag(bitstream), + this.document = new BitstreamDocumentCoverPage(etag(bitstream), coverPage.length, new ByteArrayInputStream(coverPage)); } else { - this.document = new BitstreamDocument(bitstream.getChecksum(), + this.document = new BitstreamDocumentInputstream(bitstream.getChecksum(), bitstream.getSizeBytes(), - bitstreamService.retrieve(context, bitstream)); + bitstream.getID()); } } catch (SQLException | AuthorizeException | IOException e) { throw new RuntimeException(e); @@ -158,12 +159,61 @@ However it looks like the coverpage generation is not stable (e.g. if invoked tw } private Context initializeContext() throws SQLException { - Context context = new Context(); + Context context = new Context(Context.Mode.READ_ONLY); EPerson currentUser = ePersonService.find(context, currentUserUUID); context.setCurrentUser(currentUser); currentSpecialGroups.forEach(context::setSpecialGroup); return context; } - private record BitstreamDocument(String etag, long length, InputStream inputStream) {} + protected abstract class BitstreamDocument implements InputStreamSource { + private final String etag; + private final long length; + + protected BitstreamDocument(String etag, long length) { + this.etag = etag; + this.length = length; + } + + public String getEtag() { + return etag; + } + + public long length() { + return length; + } + } + + protected class BitstreamDocumentInputstream extends BitstreamDocument { + protected final UUID bitstreamUUID; + + public BitstreamDocumentInputstream(String etag, long length, UUID bitstreamUUID) { + super(etag, length); + this.bitstreamUUID = bitstreamUUID; + } + + @Override + public InputStream getInputStream() throws IOException { + try (Context context = initializeContext()) { + return bitstreamService.retrieve(context, bitstreamService.find(context, bitstreamUUID)); + } catch (SQLException | AuthorizeException | IOException e) { + throw new RuntimeException(e); + } + } + } + + protected class BitstreamDocumentCoverPage extends BitstreamDocument { + private final ByteArrayInputStream coverpage; + + public BitstreamDocumentCoverPage(String etag, long length, ByteArrayInputStream byteArrayInputStream) { + super(etag, length); + this.coverpage = byteArrayInputStream; + } + + @Override + public InputStream getInputStream() { + return coverpage; + } + } + } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceConfigurationInitializer.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceConfigurationInitializer.java index c04ac976e0ac..d63dc8c55a11 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceConfigurationInitializer.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceConfigurationInitializer.java @@ -8,7 +8,7 @@ package org.dspace.app.rest.utils; import org.apache.commons.configuration2.Configuration; -import org.dspace.servicemanager.config.DSpaceConfigurationPropertySource; +import org.apache.commons.configuration2.spring.ConfigurationPropertySource; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; import org.springframework.context.ApplicationContextInitializer; @@ -34,8 +34,8 @@ public void initialize(final ConfigurableApplicationContext applicationContext) Configuration configuration = configurationService.getConfiguration(); // Create an Apache Commons Configuration Property Source from our configuration - DSpaceConfigurationPropertySource apacheCommonsConfigPropertySource = - new DSpaceConfigurationPropertySource(configuration.getClass().getName(), configuration); + ConfigurationPropertySource apacheCommonsConfigPropertySource = + new ConfigurationPropertySource(configuration.getClass().getName(), configuration); // Prepend it to the Environment's list of PropertySources // NOTE: This is added *first* in the list so that settings in DSpace's ConfigurationService *override* 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 88a093c0575d..77bd658b6cd6 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 */ + // JNDI usage is safe here as it loads internal DSpace configuration, not user input. @SuppressWarnings("BanJNDI") private String getDSpaceHome(ConfigurableEnvironment environment) { // Load the "dspace.dir" property from Spring Boot's Configuration (application.properties) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/HttpHeadersInitializer.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/HttpHeadersInitializer.java index d1b80c36750b..2e9bc260f49f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/HttpHeadersInitializer.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/HttpHeadersInitializer.java @@ -7,11 +7,13 @@ */ package org.dspace.app.rest.utils; -import static jakarta.mail.internet.MimeUtility.encodeText; import static java.util.Objects.isNull; import static java.util.Objects.nonNull; import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.Normalizer; import java.util.Arrays; import java.util.Collections; import java.util.Objects; @@ -171,9 +173,16 @@ public HttpHeaders initialiseHeaders() throws IOException { // distposition may be null here if contentType is null if (!isNullOrEmpty(disposition)) { - httpHeaders.put(CONTENT_DISPOSITION, Collections.singletonList(String.format(CONTENT_DISPOSITION_FORMAT, - disposition, - encodeText(fileName)))); + String fallbackAsciiName = createFallbackAsciiName(this.fileName); + String encodedUtf8Name = createEncodedUtf8Name(this.fileName); + + String headerValue = String.format( + "%s; filename=\"%s\"; filename*=UTF-8''%s", + disposition, + fallbackAsciiName, + encodedUtf8Name + ); + httpHeaders.put(CONTENT_DISPOSITION, Collections.singletonList(headerValue)); } log.debug("Content-Disposition : {}", disposition); @@ -261,4 +270,41 @@ private static boolean matches(String matchHeader, String toMatch) { return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1; } + /** + * Creates a safe ASCII-only fallback filename by removing diacritics (accents) + * and replacing any remaining non-ASCII characters. + * E.g., "ä-ö-é.pdf" becomes "a-o-e.pdf". + * @param originalFilename The original filename. + * @return A string containing only ASCII characters. + */ + private String createFallbackAsciiName(String originalFilename) { + if (originalFilename == null) { + return ""; + } + String normalized = Normalizer.normalize(originalFilename, Normalizer.Form.NFD); + String withoutAccents = normalized.replaceAll("\\p{InCombiningDiacriticalMarks}+", ""); + return withoutAccents.replaceAll("[^\\x00-\\x7F]", ""); + } + + /** + * Creates a percent-encoded UTF-8 filename according to RFC 5987. + * This is for the `filename*` parameter. + * E.g., "ä ö é.pdf" becomes "%C3%A4%20%C3%B6%20%C3%A9.pdf". + * @param originalFilename The original filename. + * @return A percent-encoded string. + */ + private String createEncodedUtf8Name(String originalFilename) { + if (originalFilename == null) { + return ""; + } + try { + String encoded = URLEncoder.encode(originalFilename, StandardCharsets.UTF_8.toString()); + return encoded.replace("+", "%20"); + } catch (java.io.UnsupportedEncodingException e) { + // Fallback to a simple ASCII name if encoding fails. + log.error("UTF-8 encoding not supported, which should not happen.", e); + return createFallbackAsciiName(originalFilename); + } + } + } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java index 4603569da84c..53f4317808a6 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java @@ -27,6 +27,7 @@ import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.handle.service.HandleService; +import org.dspace.services.ConfigurationService; import org.dspace.statistics.Dataset; import org.dspace.statistics.content.DatasetDSpaceObjectGenerator; import org.dspace.statistics.content.DatasetTimeGenerator; @@ -46,6 +47,9 @@ @Component public class UsageReportUtils { + @Autowired + private ConfigurationService configurationService; + @Autowired private HandleService handleService; @@ -135,13 +139,14 @@ public UsageReportRest createUsageReport(Context context, DSpaceObject dso, Stri */ private UsageReportRest resolveGlobalUsageReport(Context context) throws SQLException, IOException, ParseException, SolrServerException { + int topItemsLimit = configurationService.getIntProperty("usage-statistics.topItemsLimit", 10); + StatisticsListing statListing = new StatisticsListing( new StatisticsDataVisits()); - // Adding a new generator for our top 10 items without a name length delimiter + // Adding a new generator for our top n items without a name length delimiter DatasetDSpaceObjectGenerator dsoAxis = new DatasetDSpaceObjectGenerator(); - // TODO make max nr of top items (views wise)? Must be set - dsoAxis.addDsoChild(Constants.ITEM, 10, false, -1); + dsoAxis.addDsoChild(Constants.ITEM, topItemsLimit, false, -1); statListing.addDatasetGenerator(dsoAxis); Dataset dataset = statListing.getDataset(context, 1); @@ -182,7 +187,7 @@ private UsageReportRest resolveTotalVisits(Context context, DSpaceObject dso) UsageReportPointDsoTotalVisitsRest totalVisitPoint = new UsageReportPointDsoTotalVisitsRest(); totalVisitPoint.setType(StringUtils.substringAfterLast(dso.getClass().getName().toLowerCase(), ".")); totalVisitPoint.setId(dso.getID().toString()); - if (dataset.getColLabels().size() > 0) { + if (!dataset.getColLabels().isEmpty()) { totalVisitPoint.setLabel(dso.getName()); totalVisitPoint.addValue("views", Integer.valueOf(dataset.getMatrix()[0][0])); } else { @@ -205,10 +210,14 @@ private UsageReportRest resolveTotalVisits(Context context, DSpaceObject dso) */ private UsageReportRest resolveTotalVisitsPerMonth(Context context, DSpaceObject dso) throws SQLException, IOException, ParseException, SolrServerException { + String startDateInterval = + configurationService.getProperty("usage-statistics.startDateInterval", "-6"); + String endDateInterval = + configurationService.getProperty("usage-statistics.endDateInterval", "+1"); + StatisticsTable statisticsTable = new StatisticsTable(new StatisticsDataVisits(dso)); DatasetTimeGenerator timeAxis = new DatasetTimeGenerator(); - // TODO month start and end as request para? - timeAxis.setDateInterval("month", "-6", "+1"); + timeAxis.setDateInterval("month", startDateInterval, endDateInterval); statisticsTable.addDatasetGenerator(timeAxis); DatasetDSpaceObjectGenerator dsoAxis = new DatasetDSpaceObjectGenerator(); dsoAxis.addDsoChild(dso.getType(), 10, false, -1); @@ -275,7 +284,10 @@ private UsageReportRest resolveTotalDownloads(Context context, DSpaceObject dso) */ private UsageReportRest resolveTopCountries(Context context, DSpaceObject dso) throws SQLException, IOException, ParseException, SolrServerException { - Dataset dataset = this.getTypeStatsDataset(context, dso, "countryCode", 1); + int topCountriesLimit = + configurationService.getIntProperty("usage-statistics.topCountriesLimit", 100); + + Dataset dataset = this.getTypeStatsDataset(context, dso, "countryCode", topCountriesLimit, 1); UsageReportRest usageReportRest = new UsageReportRest(); for (int i = 0; i < dataset.getColLabels().size(); i++) { @@ -299,7 +311,10 @@ private UsageReportRest resolveTopCountries(Context context, DSpaceObject dso) */ private UsageReportRest resolveTopCities(Context context, DSpaceObject dso) throws SQLException, IOException, ParseException, SolrServerException { - Dataset dataset = this.getTypeStatsDataset(context, dso, "city", 1); + int topCitiesLimit = + configurationService.getIntProperty("usage-statistics.topCitiesLimit", 100); + + Dataset dataset = this.getTypeStatsDataset(context, dso, "city", topCitiesLimit, 1); UsageReportRest usageReportRest = new UsageReportRest(); for (int i = 0; i < dataset.getColLabels().size(); i++) { @@ -339,16 +354,17 @@ private Dataset getDSOStatsDataset(Context context, DSpaceObject dso, int facetM * @param dso DSO we want the stats dataset of * @param typeAxisString String of the type we want on the axis of the dataset (corresponds to solr field), * examples: countryCode, city + * @param typeAxisMax Maximum amount of results to return in the dataset * @param facetMinCount Minimum amount of results on a facet data point for it to be added to dataset * @return Stats dataset with the given type on the axis, of the given DSO and with given facetMinCount */ - private Dataset getTypeStatsDataset(Context context, DSpaceObject dso, String typeAxisString, int facetMinCount) + private Dataset getTypeStatsDataset(Context context, DSpaceObject dso, String typeAxisString, int typeAxisMax, + int facetMinCount) throws SQLException, IOException, ParseException, SolrServerException { StatisticsListing statListing = new StatisticsListing(new StatisticsDataVisits(dso)); DatasetTypeGenerator typeAxis = new DatasetTypeGenerator(); typeAxis.setType(typeAxisString); - // TODO make max nr of top countries/cities a request para? Must be set - typeAxis.setMax(100); + typeAxis.setMax(typeAxisMax); statListing.addDatasetGenerator(typeAxis); return statListing.getDataset(context, facetMinCount); } diff --git a/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/discovery.xml b/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/discovery.xml index 92917818132e..783e1e19ff5f 100644 --- a/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/discovery.xml +++ b/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/discovery.xml @@ -30,8 +30,6 @@ - - @@ -2250,7 +2248,7 @@ - + @@ -2261,7 +2259,7 @@ - + @@ -2275,7 +2273,7 @@ - + @@ -2287,7 +2285,7 @@ - + @@ -2299,7 +2297,7 @@ - + @@ -2311,7 +2309,7 @@ - + @@ -2323,7 +2321,7 @@ - + @@ -2334,7 +2332,7 @@ - + @@ -2346,7 +2344,7 @@ - + @@ -2358,7 +2356,7 @@ - + @@ -2370,7 +2368,7 @@ - + @@ -2382,7 +2380,7 @@ - + @@ -3265,7 +3263,7 @@ - + diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AbstractLiveImportIntegrationTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AbstractLiveImportIntegrationTest.java index cf3e125cc531..94fbce4a5115 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AbstractLiveImportIntegrationTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AbstractLiveImportIntegrationTest.java @@ -16,12 +16,12 @@ import java.util.ArrayList; import java.util.List; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.ProtocolVersion; import org.apache.http.StatusLine; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.entity.BasicHttpEntity; -import org.apache.tools.ant.filters.StringInputStream; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.importer.external.datamodel.ImportRecord; import org.dspace.importer.external.metadatamapping.MetadatumDTO; @@ -43,7 +43,8 @@ protected void matchRecords(ArrayList recordsImported, ArrayList list, List list2) { assertEquals(list.size(), list2.size()); for (int i = 0; i < list.size(); i++) { - assertTrue(sameMetadatum(list.get(i), list2.get(i))); + assertTrue("'" + list.get(i).toString() + "' should be equal to '" + list2.get(i).toString() + "'", + sameMetadatum(list.get(i), list2.get(i))); } } @@ -70,7 +71,7 @@ protected CloseableHttpResponse mockResponse(String xmlExample, int statusCode, throws UnsupportedEncodingException { BasicHttpEntity basicHttpEntity = new BasicHttpEntity(); basicHttpEntity.setChunked(true); - basicHttpEntity.setContent(new StringInputStream(xmlExample)); + basicHttpEntity.setContent(IOUtils.toInputStream(xmlExample)); CloseableHttpResponse response = mock(CloseableHttpResponse.class); when(response.getStatusLine()).thenReturn(statusLine(statusCode, reason)); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java index 9edb0a2a9f40..b4c3e720cfc6 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java @@ -1803,6 +1803,102 @@ private boolean tokenClaimsEqual(String token1, String token2) { } } + @Test + public void testShibbolethStaffMappedToStaffAndMembers() throws Exception { + context.turnOffAuthorisationSystem(); + + GroupBuilder.createGroup(context) + .withName("Staff") + .build(); + GroupBuilder.createGroup(context) + .withName("Member") + .build(); + + configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY); + configurationService.setProperty("authentication-shibboleth.role.staff", "Staff, Member"); + configurationService.setProperty("authentication-shibboleth.default-roles", "staff"); + configurationService.setProperty("authentication-shibboleth.netid-header", "mail"); + configurationService.setProperty("authentication-shibboleth.email-header", "mail"); + + context.restoreAuthSystemState(); + + String shibToken = getClient().perform(post("/api/authn/login") + .requestAttr("mail", eperson.getEmail()) + .requestAttr("SHIB-SCOPED-AFFILIATION", "staff")) + .andExpect(status().isOk()) + .andReturn().getResponse().getHeader(AUTHORIZATION_HEADER).replace(AUTHORIZATION_TYPE, ""); + + getClient(shibToken).perform(get("/api/authn/status").param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.okay", is(true))) + .andExpect(jsonPath("$.authenticated", is(true))) + .andExpect(jsonPath("$.authenticationMethod", is("shibboleth"))) + .andExpect(jsonPath("$._embedded.specialGroups._embedded.specialGroups", + Matchers.containsInAnyOrder( + matchGroupWithName("Staff"), + matchGroupWithName("Member") + ) + )); + + getClient(shibToken).perform(get("/api/authn/status/specialGroups").param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.specialGroups", + Matchers.containsInAnyOrder( + matchGroupWithName("Staff"), + matchGroupWithName("Member") + ) + )); + } + + @Test + public void testPasswordLoginNotMappedToStaffAndMembers() throws Exception { + context.turnOffAuthorisationSystem(); + + GroupBuilder.createGroup(context) + .withName("Staff") + .build(); + GroupBuilder.createGroup(context) + .withName("Member") + .build(); + GroupBuilder.createGroup(context) + .withName("specialGroupPwd") + .build(); + + + configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", + "org.dspace.authenticate.PasswordAuthentication, org.dspace.authenticate.ShibAuthentication"); + configurationService.setProperty("authentication-shibboleth.role.staff", "Staff, Member"); + configurationService.setProperty("authentication-shibboleth.default-roles", "staff"); + configurationService.setProperty("authentication-shibboleth.netid-header", "mail"); + configurationService.setProperty("authentication-shibboleth.email-header", "mail"); + configurationService.setProperty("authentication-password.login.specialgroup", "specialGroupPwd"); + + context.restoreAuthSystemState(); + + String passwordToken = getAuthToken(eperson.getEmail(), password); + + getClient(passwordToken).perform(get("/api/authn/status").param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.okay", is(true))) + .andExpect(jsonPath("$.authenticated", is(true))) + .andExpect(jsonPath("$.authenticationMethod", is("password"))) + .andExpect(jsonPath("$._embedded.specialGroups._embedded.specialGroups", + Matchers.containsInAnyOrder( + matchGroupWithName("specialGroupPwd") + ) + )); + + getClient(passwordToken).perform(get("/api/authn/status/specialGroups").param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.specialGroups", + Matchers.containsInAnyOrder( + matchGroupWithName("specialGroupPwd") + ) + )); + } + + + private OrcidTokenResponseDTO buildOrcidTokenResponse(String orcid, String accessToken) { OrcidTokenResponseDTO token = new OrcidTokenResponseDTO(); token.setAccessToken(accessToken); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java index 1e7d6440ff24..179e93656d9e 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java @@ -7,7 +7,6 @@ */ package org.dspace.app.rest; -import static jakarta.mail.internet.MimeUtility.encodeText; import static java.util.UUID.randomUUID; import static org.apache.commons.codec.CharEncoding.UTF_8; import static org.apache.commons.collections.CollectionUtils.isEmpty; @@ -90,6 +89,7 @@ import org.dspace.statistics.factory.StatisticsServiceFactory; import org.dspace.statistics.service.SolrLoggerService; import org.dspace.storage.bitstore.factory.StorageServiceFactory; +import org.dspace.storage.bitstore.service.BitstreamStorageService; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -350,7 +350,11 @@ public void testBitstreamName() throws Exception { //2. A public item with a bitstream String bitstreamContent = "0123456789"; - String bitstreamName = "ภาษาไทย"; + String bitstreamName = "ภาษาไทย-com-acentuação.pdf"; + String expectedAscii = "-com-acentuacao.pdf"; + String expectedUtf8Encoded = + "%E0%B8%A0%E0%B8%B2%E0%B8%A9%E0%B8%B2%E0%B9%84%E0%B8%97%E0%B8%A2-" + + "com-acentua%C3%A7%C3%A3o.pdf"; try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { @@ -374,7 +378,9 @@ public void testBitstreamName() throws Exception { //We expect the content disposition to have the encoded bitstream name .andExpect(header().string( "Content-Disposition", - "attachment;filename=\"" + encodeText(bitstreamName) + "\"" + String.format("attachment; filename=\"%s\"; filename*=UTF-8''%s", + expectedAscii, + expectedUtf8Encoded) )); } @@ -1256,12 +1262,8 @@ public void closeInputStreamsDownloadWithCoverPage() throws Exception { @Test public void checkContentDispositionOfFormats() throws Exception { - configurationService.setProperty("webui.content_disposition_format", new String[] { - "text/richtext", - "text/xml", - "txt" - }); - + // This test verifies that, by default, common text formats will be downloaded instead of being served inline. + // The next two tests will verify behavior of non-default settings of "webui.content_disposition_inline" context.turnOffAuthorisationSystem(); Community community = CommunityBuilder.createCommunity(context).build(); Collection collection = CollectionBuilder.createCollection(context, community).build(); @@ -1283,18 +1285,26 @@ public void checkContentDispositionOfFormats() throws Exception { } context.restoreAuthSystemState(); - // these formats are configured and files should be downloaded + // Based on default configuration all files should be downloaded verifyBitstreamDownload(rtf, "text/richtext;charset=UTF-8", true); verifyBitstreamDownload(xml, "text/xml;charset=UTF-8", true); verifyBitstreamDownload(txt, "text/plain;charset=UTF-8", true); - // this format is not configured and should open inline - verifyBitstreamDownload(csv, "text/csv;charset=UTF-8", false); + verifyBitstreamDownload(csv, "text/csv;charset=UTF-8", true); } @Test - public void checkHardcodedContentDispositionFormats() throws Exception { - // This test is similar to the above test, but it verifies that our *hardcoded settings* for - // webui.content_disposition_format are protecting us from loading specific formats *inline*. + public void checkBannedContentDispositionInlineFormats() throws Exception { + configurationService.setProperty("webui.content_disposition_inline", new String[] { + "text/html", + "text/javascript", + "rdf", + "text/xml", + "image/svg+xml" + }); + + // This test is similar to the above test, but it verifies that if a site specifies + // a banned format (e.g. HTML, XML, etc) in their "webui.content_disposition_inline" setting + // DSpace will still protect them by refusing to load the format *inline*. context.turnOffAuthorisationSystem(); Community community = CommunityBuilder.createCommunity(context).build(); Collection collection = CollectionBuilder.createCollection(context, community).build(); @@ -1324,8 +1334,9 @@ public void checkHardcodedContentDispositionFormats() throws Exception { } context.restoreAuthSystemState(); - // By default, HTML, JS & XML should all download. This protects us from possible XSS attacks, as - // each of these formats can embed JavaScript which may execute when the file is loaded *inline*. + // By default, HTML, JS & XML should all download regardless of inline configuration. + // This protects us from possible XSS attacks, as each of these formats can embed JavaScript + // which may execute when the file is loaded *inline*. verifyBitstreamDownload(html, "text/html;charset=UTF-8", true); verifyBitstreamDownload(js, "text/javascript;charset=UTF-8", true); verifyBitstreamDownload(rdf, "application/rdf+xml;charset=UTF-8", true); @@ -1338,22 +1349,29 @@ public void checkHardcodedContentDispositionFormats() throws Exception { } @Test - public void checkWildcardContentDispositionFormats() throws Exception { - // Setting "*" should result in all formats being downloaded (nothing will be opened inline) - configurationService.setProperty("webui.content_disposition_format", "*"); - + public void checkContentDispositionInlineFormats() throws Exception { + // Set PDF and a few image formats to verify they will display inline. But leave off "text/plain" + configurationService.setProperty("webui.content_disposition_inline", new String[] { + "text/csv", + "application/pdf", + "image/jpeg", + "video/mpeg" + }); context.turnOffAuthorisationSystem(); Community community = CommunityBuilder.createCommunity(context).build(); Collection collection = CollectionBuilder.createCollection(context, community).build(); Item item = ItemBuilder.createItem(context, collection).build(); String content = "Test Content"; Bitstream csv; + Bitstream txt; Bitstream jpg; Bitstream mpg; Bitstream pdf; try (InputStream is = IOUtils.toInputStream(content, CharEncoding.UTF_8)) { csv = BitstreamBuilder.createBitstream(context, item, is) .withMimeType("text/csv").build(); + txt = BitstreamBuilder.createBitstream(context, item, is) + .withMimeType("text/plain").build(); jpg = BitstreamBuilder.createBitstream(context, item, is) .withMimeType("image/jpeg").build(); mpg = BitstreamBuilder.createBitstream(context, item, is) @@ -1363,11 +1381,13 @@ public void checkWildcardContentDispositionFormats() throws Exception { } context.restoreAuthSystemState(); - // All formats should be download only - verifyBitstreamDownload(csv, "text/csv;charset=UTF-8", true); - verifyBitstreamDownload(jpg, "image/jpeg;charset=UTF-8", true); - verifyBitstreamDownload(mpg, "video/mpeg;charset=UTF-8", true); - verifyBitstreamDownload(pdf, "application/pdf;charset=UTF-8", true); + // Only text/plain should download, while other formats should be served inline based on the configuration + verifyBitstreamDownload(csv, "text/csv;charset=UTF-8", false); + verifyBitstreamDownload(jpg, "image/jpeg;charset=UTF-8", false); + verifyBitstreamDownload(mpg, "video/mpeg;charset=UTF-8", false); + verifyBitstreamDownload(pdf, "application/pdf;charset=UTF-8", false); + // This is the only format not listed in the inline configuration, so it will be downloaded + verifyBitstreamDownload(txt, "text/plain;charset=UTF-8", true); } @@ -1502,4 +1522,173 @@ private void givenPdf(boolean coverPageEnabled, ThrowingConsumer block) { } } + @Test + public void bitstreamInputStreamClosesWithGetRequestTest() throws Exception { + InputStream realStream = new ByteArrayInputStream("abc".getBytes()); + InputStream spyStream = Mockito.spy(realStream); + + context.turnOffAuthorisationSystem(); + Community community = CommunityBuilder.createCommunity(context).build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + Item item = ItemBuilder.createItem(context, collection).build(); + + Bitstream bitstream = BitstreamBuilder.createBitstream(context, item, realStream).build(); + context.restoreAuthSystemState(); + + BitstreamStorageService originalService = + StorageServiceFactory.getInstance().getBitstreamStorageService(); + BitstreamStorageService spyService = spy(originalService); + + doReturn(spyStream).when(spyService).retrieve(any(), eq(bitstream)); + + ReflectionTestUtils.setField(bitstreamService, "bitstreamStorageService", spyService); + + try { + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")) + .andExpect(status().isOk()); + + boolean bitstreamRetrieved = Mockito.mockingDetails(spyService) + .getInvocations().stream() + .filter(i -> i.getMethod().getName().equals("retrieve")) + .mapToInt(i -> 1) + .sum() > 0; + + if (bitstreamRetrieved) { + Mockito.verify(spyStream, times(1) + .description("InputStream should have been closed after GET request")) + .close(); + } + } finally { + ReflectionTestUtils.setField(bitstreamService, "bitstreamStorageService", originalService); + } + } + + @Test + public void bitstreamInputStreamClosesWithHeadRequestTest() throws Exception { + InputStream realStream = new ByteArrayInputStream("abc".getBytes()); + InputStream spyStream = Mockito.spy(realStream); + + context.turnOffAuthorisationSystem(); + Community community = CommunityBuilder.createCommunity(context).build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + Item item = ItemBuilder.createItem(context, collection).build(); + + Bitstream bitstream = BitstreamBuilder.createBitstream(context, item, realStream).build(); + context.restoreAuthSystemState(); + + BitstreamStorageService originalService = + StorageServiceFactory.getInstance().getBitstreamStorageService(); + BitstreamStorageService spyService = spy(originalService); + + doReturn(spyStream).when(spyService).retrieve(any(), eq(bitstream)); + + ReflectionTestUtils.setField(bitstreamService, "bitstreamStorageService", spyService); + + try { + getClient().perform(head("/api/core/bitstreams/" + bitstream.getID() + "/content")) + .andExpect(status().isOk()); + + boolean bitstreamRetrieved = Mockito.mockingDetails(spyService) + .getInvocations().stream() + .filter(i -> i.getMethod().getName().equals("retrieve")) + .mapToInt(i -> 1) + .sum() > 0; + + if (bitstreamRetrieved) { + Mockito.verify(spyStream, times(1) + .description("InputStream should have been closed after HEAD request")) + .close(); + } + } finally { + ReflectionTestUtils.setField(bitstreamService, "bitstreamStorageService", originalService); + } + } + + @Test + public void bitstreamInputStreamClosesWithGetRequestAndCitationPageEnabledTest() throws Exception { + configurationService.setProperty("citation-page.enable_globally", true); + citationDocumentService.afterPropertiesSet(); + + InputStream realStream = new ByteArrayInputStream("abc".getBytes()); + InputStream spyStream = Mockito.spy(realStream); + + context.turnOffAuthorisationSystem(); + Community community = CommunityBuilder.createCommunity(context).build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + Item item = ItemBuilder.createItem(context, collection).build(); + + Bitstream bitstream = BitstreamBuilder.createBitstream(context, item, realStream).build(); + context.restoreAuthSystemState(); + + BitstreamStorageService originalService = + StorageServiceFactory.getInstance().getBitstreamStorageService(); + BitstreamStorageService spyService = spy(originalService); + + doReturn(spyStream).when(spyService).retrieve(any(), eq(bitstream)); + + ReflectionTestUtils.setField(bitstreamService, "bitstreamStorageService", spyService); + + try { + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")) + .andExpect(status().isOk()); + + boolean bitstreamRetrieved = Mockito.mockingDetails(spyService) + .getInvocations().stream() + .filter(i -> i.getMethod().getName().equals("retrieve")) + .mapToInt(i -> 1) + .sum() > 0; + + if (bitstreamRetrieved) { + Mockito.verify(spyStream, times(1) + .description("InputStream should have been closed after GET request")) + .close(); + } + } finally { + ReflectionTestUtils.setField(bitstreamService, "bitstreamStorageService", originalService); + } + } + + @Test + public void bitstreamInputStreamClosesWithHeadRequestAndCitationPageEnabledTest() throws Exception { + configurationService.setProperty("citation-page.enable_globally", true); + citationDocumentService.afterPropertiesSet(); + + InputStream realStream = new ByteArrayInputStream("abc".getBytes()); + InputStream spyStream = Mockito.spy(realStream); + + context.turnOffAuthorisationSystem(); + Community community = CommunityBuilder.createCommunity(context).build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + Item item = ItemBuilder.createItem(context, collection).build(); + + Bitstream bitstream = BitstreamBuilder.createBitstream(context, item, realStream).build(); + context.restoreAuthSystemState(); + + BitstreamStorageService originalService = + StorageServiceFactory.getInstance().getBitstreamStorageService(); + BitstreamStorageService spyService = spy(originalService); + + doReturn(spyStream).when(spyService).retrieve(any(), eq(bitstream)); + + ReflectionTestUtils.setField(bitstreamService, "bitstreamStorageService", spyService); + + try { + getClient().perform(head("/api/core/bitstreams/" + bitstream.getID() + "/content")) + .andExpect(status().isOk()); + + boolean bitstreamRetrieved = Mockito.mockingDetails(spyService) + .getInvocations().stream() + .filter(i -> i.getMethod().getName().equals("retrieve")) + .mapToInt(i -> 1) + .sum() > 0; + + if (bitstreamRetrieved) { + Mockito.verify(spyStream, times(1) + .description("InputStream should have been closed after HEAD request")) + .close(); + } + } finally { + ReflectionTestUtils.setField(bitstreamService, "bitstreamStorageService", originalService); + } + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java index 1ed1e23260f9..eaef4bd23f9a 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java @@ -1885,8 +1885,8 @@ public void testBrowseByItemsStartsWith() throws Exception { // ---- BROWSES BY ITEM ---- //** WHEN ** //An anonymous user browses the items in the Browse by date issued endpoint - //with startsWith set to 199 - getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=199") + //with startsWith set to 1990 + getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=1990") .param("size", "2")) //** THEN ** @@ -1895,8 +1895,8 @@ public void testBrowseByItemsStartsWith() throws Exception { //We expect the content type to be "application/hal+json;charset=UTF-8" .andExpect(content().contentType(contentType)) - //We expect the totalElements to be the 2 items present in the repository - .andExpect(jsonPath("$.page.totalElements", is(2))) + //We expect the totalElements to be the 5 items from 1990 til now + .andExpect(jsonPath("$.page.totalElements", is(5))) //We expect to jump to page 1 of the index .andExpect(jsonPath("$.page.number", is(0))) .andExpect(jsonPath("$.page.size", is(2))) @@ -2057,8 +2057,8 @@ public void testBrowseByStartsWithAndPage() throws Exception { //** WHEN ** //An anonymous user browses the items in the Browse by date issued endpoint - //with startsWith set to 199 and Page to 1 - getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=199") + //with startsWith set to 1990 and Page to 1 + getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=1990") .param("size", "1").param("page", "1")) //** THEN ** @@ -2067,17 +2067,76 @@ public void testBrowseByStartsWithAndPage() throws Exception { //We expect the content type to be "application/hal+json;charset=UTF-8" .andExpect(content().contentType(contentType)) - //We expect the totalElements to be the 2 items present in the repository - .andExpect(jsonPath("$.page.totalElements", is(2))) + //We expect the totalElements to be the 5 items present in the repository from 1990 until now + .andExpect(jsonPath("$.page.totalElements", is(5))) //We expect to jump to page 1 of the index .andExpect(jsonPath("$.page.number", is(1))) .andExpect(jsonPath("$.page.size", is(1))) - .andExpect(jsonPath("$._links.self.href", containsString("startsWith=199"))) + .andExpect(jsonPath("$._links.self.href", containsString("startsWith=1990"))) - //Verify that the index jumps to the "Java" item. - .andExpect(jsonPath("$._embedded.items", - contains( - ItemMatcher.matchItemWithTitleAndDateIssued(item3, "Java", "1995-05-23") + //Verify that the returned item is 2nd (page 0 first item, page 1 second item) item from 1990 + // Items: Alan Turing - 1912; Blade Runner - 1982-06-25 || Python - 1990; + // Java - 1995-05-23; Zeta Reticuli - 2018-01-01; Moon - 2018-01-02; T-800 - 2029 + // 2nd since 1990: Java + .andExpect(jsonPath("$._embedded.items", + contains(ItemMatcher.matchItemWithTitleAndDateIssued(item3, + "Java", "1995-05-23") + ))); + + getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=1990") + .param("size", "2").param("page", "1")) + //Verify that the returned item is 3rd&4th item from 1990 + // Items: Alan Turing - 1912; Blade Runner - 1982-06-25 || Python - 1990; + // Java - 1995-05-23; Zeta Reticuli - 2018-01-01; Moon - 2018-01-02; T-800 - 2029 + // => Zeta Reticuli & Moon + .andExpect(jsonPath("$._embedded.items", + contains(ItemMatcher.matchItemWithTitleAndDateIssued(item7, + "Zeta Reticuli", "2018-01-01"), + ItemMatcher.matchItemWithTitleAndDateIssued(item4, + "Moon", "2018-01-02") + ))); + + // Sort descending + getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=1990&sort=default,DESC") + .param("size", "2").param("page", "0")) + //Verify that the returned items are from 1990 and below dates + // Items: Alan Turing - 1912; Blade Runner - 1982-06-25 || Python - 1990; + // Java - 1995-05-23; Zeta Reticuli - 2018-01-01; Moon - 2018-01-02; T-800 - 2029 + // => Python & Blade Runner + .andExpect(jsonPath("$._embedded.items", + contains(ItemMatcher.matchItemWithTitleAndDateIssued(item5, + "Python", "1990"), + ItemMatcher.matchItemWithTitleAndDateIssued(item2, + "Blade Runner", "1982-06-25") + ))); + + getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=1990&sort=default,DESC") + .param("size", "1").param("page", "0")) + //Verify that the returned item is the one closest to 1990 but below its upperBound (1990-12-31) + .andExpect(jsonPath("$._embedded.items", + contains(ItemMatcher.matchItemWithTitleAndDateIssued(item5, + "Python", "1990") + ))); + + getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=1990&sort=default,DESC") + .param("size", "3").param("page", "0")) + //Verify that the 3 returned items are from 1990 and below dates, + // with closest to upperBound 1990-12-31 as first + .andExpect(jsonPath("$._embedded.items", + contains(ItemMatcher.matchItemWithTitleAndDateIssued(item5, + "Python", "1990"), + ItemMatcher.matchItemWithTitleAndDateIssued(item2, + "Blade Runner", "1982-06-25"), + ItemMatcher.matchItemWithTitleAndDateIssued(item1, + "Alan Turing", "1912-06-23") + ))); + + getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=1982-06&sort=default,DESC") + .param("size", "1").param("page", "0")) + //Verify that the returned item is the one closest to 1982-06 but below its upperBound (1982-06-30) + .andExpect(jsonPath("$._embedded.items", + contains(ItemMatcher.matchItemWithTitleAndDateIssued(item2, + "Blade Runner", "1982-06-25") ))); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java index 8ddcbd6ad26a..90ee769d42b9 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java @@ -56,6 +56,7 @@ import org.dspace.app.rest.projection.Projection; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.app.rest.test.MetadataPatchSuite; +import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.AuthorizeService; import org.dspace.authorize.service.ResourcePolicyService; import org.dspace.builder.CollectionBuilder; @@ -1473,6 +1474,7 @@ public void updateCollectionEpersonWithWriteRightsTest() throws Exception { authorizeService.removePoliciesActionFilter(context, eperson, Constants.WRITE); } + @Test public void patchCollectionMetadataAuthorized() throws Exception { runPatchMetadataTests(admin, 200); } @@ -3314,6 +3316,109 @@ public void addColAdminGroupToCheckReindexingTest() throws Exception { .andExpect(jsonPath("$.page.totalElements", is(1))); } + @Test + public void addParentComAdminGroupToCheckAdminPropagationTest() throws Exception { + addParentComAdminGroupToCheckGenericPropagationTest("findAdminAuthorized"); + } + + @Test + public void addParentComAdminGroupToCheckEditPropagationTest() throws Exception { + addParentComAdminGroupToCheckGenericPropagationTest("findEditAuthorized"); + } + + public void addParentComAdminGroupToCheckGenericPropagationTest(String method) throws Exception { + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("MyTest") + .build(); + + context.restoreAuthSystemState(); + + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken).perform(get("/api/core/collections/search/" + method) + .param("query", "MyTest")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + AtomicReference idRef = new AtomicReference<>(); + ObjectMapper mapper = new ObjectMapper(); + GroupRest groupRest = new GroupRest(); + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(post("/api/core/communities/" + parentCommunity.getID() + "/adminGroup") + .content(mapper.writeValueAsBytes(groupRest)) + .contentType(contentType)) + .andExpect(status().isCreated()) + .andDo(result -> idRef.set( + UUID.fromString(read(result.getResponse().getContentAsString(), "$.id"))) + ); + + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken).perform(post("/api/eperson/groups/" + idRef.get() + "/epersons") + .contentType(parseMediaType(TEXT_URI_LIST_VALUE)) + .content(REST_SERVER_URL + "eperson/groups/" + eperson.getID() + )); + + getClient(epersonToken).perform(get("/api/core/collections/search/" + method) + .param("query", "MyTest")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.collections", Matchers.contains(CollectionMatcher + .matchProperties(col1.getName(), col1.getID(), col1.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void removeParentComAdminPolicyToCheckAdminPropagationTest() throws Exception { + removeParentComAdminPolicyToCheckGenericPropagationTest("findAdminAuthorized"); + } + + @Test + public void removeParentComAdminPolicyToCheckEditPropagationTest() throws Exception { + removeParentComAdminPolicyToCheckGenericPropagationTest("findEditAuthorized"); + } + + public void removeParentComAdminPolicyToCheckGenericPropagationTest(String method) throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + ResourcePolicy policy = ResourcePolicyBuilder.createResourcePolicy(context, eperson, null) + .withDspaceObject(parentCommunity).withAction(Constants.ADMIN) + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("MyTest") + .build(); + + context.restoreAuthSystemState(); + + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken).perform(get("/api/core/collections/search/" + method) + .param("query", "MyTest")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.collections", Matchers.contains(CollectionMatcher + .matchProperties(col1.getName(), col1.getID(), col1.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(delete("/api/authz/resourcepolicies/" + policy.getID())) + .andExpect(status().is(204)); + + getClient(epersonToken).perform(get("/api/core/collections/search/" + method) + .param("query", "MyTest")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + } + @Test public void findAuthorizedCollectionsByEntityType() throws Exception { context.turnOffAuthorisationSystem(); @@ -3652,4 +3757,296 @@ public void findSubmitAuthorizedByCommunityAndEntityTypeNotFoundTest() throws Ex .andExpect(status().isNotFound()); } + @Test + public void findEditAuthorizedUnauthorizedTest() throws Exception { + getClient().perform(get("/api/core/collections/search/findEditAuthorized")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findEditAuthorizedResourcePolicyTest() throws Exception { + context.turnOffAuthorisationSystem(); + Community comm1 = CommunityBuilder.createCommunity(context).withName("Community 1").build(); + + EPerson hasDirectEditRights = EPersonBuilder.createEPerson(context) + .withEmail("has@editrights.com").withPassword(password) + .build(); + EPerson hasDirectAdminRights = EPersonBuilder.createEPerson(context) + .withEmail("has@adminrights.com").withPassword(password) + .build(); + Collection byResourcePolicy = CollectionBuilder.createCollection(context, comm1) + .withName("direct edit rights for eperson") + .build(); + Collection uneditable = CollectionBuilder.createCollection(context, comm1) + .withName("uneditable collection") + .build(); + ResourcePolicy policy = ResourcePolicyBuilder.createResourcePolicy(context, hasDirectEditRights, null) + .withDspaceObject(byResourcePolicy).withAction(WRITE) + .build(); + policy = ResourcePolicyBuilder.createResourcePolicy(context, hasDirectAdminRights, null) + .withDspaceObject(byResourcePolicy).withAction(Constants.ADMIN) + .build(); + context.restoreAuthSystemState(); + + String tokenHasDirectEditRightsToken = getAuthToken(hasDirectEditRights.getEmail(), password); + String tokenHasDirectAdminRightsToken = getAuthToken(hasDirectAdminRights.getEmail(), password); + + getClient(tokenHasDirectEditRightsToken).perform(get("/api/core/collections/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.collections", + Matchers.contains(CollectionMatcher.matchCollection(byResourcePolicy)))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + getClient(tokenHasDirectAdminRightsToken).perform(get("/api/core/collections/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.collections", + Matchers.contains(CollectionMatcher.matchCollection(byResourcePolicy)))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void findEditAuthorizedAdminPropagationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + /* + DSO structure: + root + ├── subcomm1 + ├── subcomm1collA (collection) + └── subcomm2subcomm3 (community) + ├── subcomm2subcomm3collB (collection) + └── subcomm2 + └── subcomm2coll + */ + EPerson rootAdmin = EPersonBuilder.createEPerson(context) + .withEmail("root@admin.com").withPassword(password).build(); + EPerson subcomm1Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm1@admin.com").withPassword(password).build(); + EPerson subcomm2Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm2@admin.com").withPassword(password).build(); + EPerson subcomm1collA_Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm1collA@admin.com").withPassword(password).build(); + EPerson subcomm1collB_Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm1collB@admin.com").withPassword(password).build(); + EPerson subcomm2collAdmin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm2coll@admin.com").withPassword(password).build(); + + Community root = CommunityBuilder.createCommunity(context) + .withAdminGroup(rootAdmin) + .withName("root") + .build(); + Community subcomm1 = CommunityBuilder.createSubCommunity(context, root) + .withAdminGroup(subcomm1Admin) + .withName("subcomm1") + .build(); + Community subcomm1subcom3 = CommunityBuilder.createSubCommunity(context, subcomm1) + .withName("subcomm1subcom3") + .build(); + Community subcomm2 = CommunityBuilder.createSubCommunity(context, root) + .withAdminGroup(subcomm2Admin) + .withName("subcomm2") + .build(); + Collection subcomm1collA = CollectionBuilder.createCollection(context, subcomm1) + .withAdminGroup(subcomm1collA_Admin) + .withName("subcomm1collA") + .build(); + Collection subcomm2subcomm3collB = CollectionBuilder.createCollection(context, subcomm1subcom3) + .withAdminGroup(subcomm1collB_Admin) + .withName("subcomm2subcomm3collB") + .build(); + Collection subcomm2coll = CollectionBuilder.createCollection(context, subcomm2) + .withAdminGroup(subcomm2collAdmin) + .withName("subcomm2coll") + .build(); + context.restoreAuthSystemState(); + + String siteAdminToken = getAuthToken(admin.getEmail(), password); + String rootAdminToken = getAuthToken(rootAdmin.getEmail(), password); + String subcomm1AdminToken = getAuthToken(subcomm1Admin.getEmail(), password); + String subcomm2AdminToken = getAuthToken(subcomm2Admin.getEmail(), password); + + getClient(siteAdminToken).perform(get("/api/core/collections/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.collections", + Matchers.containsInAnyOrder( + CollectionMatcher.matchCollection(subcomm1collA), + CollectionMatcher.matchCollection(subcomm2subcomm3collB), + CollectionMatcher.matchCollection(subcomm2coll) + ))) + .andExpect(jsonPath("$.page.totalElements", is(3))); + + getClient(rootAdminToken).perform(get("/api/core/collections/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.collections", + Matchers.containsInAnyOrder( + CollectionMatcher.matchCollection(subcomm1collA), + CollectionMatcher.matchCollection(subcomm2subcomm3collB), + CollectionMatcher.matchCollection(subcomm2coll) + ))) + .andExpect(jsonPath("$.page.totalElements", is(3))); + + getClient(subcomm1AdminToken).perform(get("/api/core/collections/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.collections", + Matchers.containsInAnyOrder( + CollectionMatcher.matchCollection(subcomm1collA), + CollectionMatcher.matchCollection(subcomm2subcomm3collB) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + getClient(subcomm2AdminToken).perform(get("/api/core/collections/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.collections", + Matchers.containsInAnyOrder( + CollectionMatcher.matchCollection(subcomm2coll) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void findEditAuthorizedCollectionsWithQueryTest() throws Exception { + findGenericAuthorizedCollectionsWithQueryTest("findEditAuthorized"); + } + + @Test + public void findReadAuthorizedCollectionsWithQueryTest() throws Exception { + findGenericAuthorizedCollectionsWithQueryTest("findAdminAuthorized"); + } + + public void findGenericAuthorizedCollectionsWithQueryTest(String method) throws Exception { + + context.turnOffAuthorisationSystem(); + + EPerson eperson2 = EPersonBuilder.createEPerson(context) + .withEmail("eperson2@mail.com") + .withPassword(password) + .build(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Community child2 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community Two") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Sample collection") + .withAdminGroup(eperson) + .build(); + Collection col2 = CollectionBuilder.createCollection(context, child1) + .withName("Test collection") + .build(); + Collection col3 = CollectionBuilder.createCollection(context, child2) + .withName("Collection of sample items") + .withAdminGroup(eperson) + .build(); + Collection col4 = CollectionBuilder.createCollection(context, child2) + .withName("Testing autocomplete in collection") + .withAdminGroup(eperson2) + .build(); + Collection col5 = CollectionBuilder.createCollection(context, child2) + .withName("Title: subtitle (special characters)") + .build(); + context.restoreAuthSystemState(); + + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + // Test simple query matches + getClient(tokenEPerson).perform(get("/api/core/collections/search/" + method) + .param("query", "collection")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.collections", Matchers.containsInAnyOrder( + CollectionMatcher.matchProperties(col1.getName(), col1.getID(), col1.getHandle()), + CollectionMatcher.matchProperties(col3.getName(), col3.getID(), col3.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test insensitive matches + getClient(tokenEPerson).perform(get("/api/core/collections/search/" + method) + .param("query", "COLLECTION")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.collections", Matchers.containsInAnyOrder( + CollectionMatcher.matchProperties(col1.getName(), col1.getID(), col1.getHandle()), + CollectionMatcher.matchProperties(col3.getName(), col3.getID(), col3.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test collection unathorized for eperson is not returned + getClient(tokenEPerson).perform(get("/api/core/collections/search/" + method) + .param("query", "test")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + // Test eperson with no authorized collections + getClient(tokenEPerson).perform(get("/api/core/collections/search/" + method) + .param("query", "auto")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + String tokenEPerson2 = getAuthToken(eperson2.getEmail(), password); + // Test eperson2 gets only their authorized collection + getClient(tokenEPerson2).perform(get("/api/core/collections/search/" + method) + .param("query", "auto")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.collections", Matchers.contains( + CollectionMatcher.matchProperties(col4.getName(), col4.getID(), col4.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Test query with multiple words + getClient(tokenEPerson2).perform(get("/api/core/collections/search/" + method) + .param("query", "testing auto")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.collections", Matchers.containsInAnyOrder( + CollectionMatcher.matchProperties(col4.getName(), col4.getID(), col4.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Test admin gets all authorized collections + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/core/collections/search/" + method) + .param("query", "sample")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.collections", Matchers.containsInAnyOrder( + CollectionMatcher.matchProperties(col1.getName(), col1.getID(), col1.getHandle()), + CollectionMatcher.matchProperties(col3.getName(), col3.getID(), col3.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test query with unsorted query words + getClient(tokenAdmin).perform(get("/api/core/collections/search/" + method) + .param("query", "items sample")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.collections", Matchers.contains( + CollectionMatcher.matchProperties(col3.getName(), col3.getID(), col3.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Test collection not authorized for eperson is returned for admin + getClient(tokenAdmin).perform(get("/api/core/collections/search/" + method) + .param("query", "test")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.collections", Matchers.containsInAnyOrder( + CollectionMatcher.matchProperties(col2.getName(), col2.getID(), col2.getHandle()), + CollectionMatcher.matchProperties(col4.getName(), col4.getID(), col4.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test query with special characters + getClient(tokenAdmin).perform(get("/api/core/collections/search/" + method) + .param("query", "title: subtitle (special")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.collections", Matchers.contains( + CollectionMatcher.matchProperties(col5.getName(), col5.getID(), col5.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CommunityRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CommunityRestRepositoryIT.java index 6837b8900398..74c5c4cf4d76 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CommunityRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CommunityRestRepositoryIT.java @@ -50,6 +50,7 @@ import org.dspace.app.rest.projection.Projection; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.app.rest.test.MetadataPatchSuite; +import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.AuthorizeService; import org.dspace.authorize.service.ResourcePolicyService; import org.dspace.builder.CollectionBuilder; @@ -2763,4 +2764,519 @@ public void addComAdminGroupToCheckReindexingTest() throws Exception { .andExpect(jsonPath("$.page.totalElements", is(1))); } + @Test + public void addParentComAdminGroupToCheckAdminPropagationTest() throws Exception { + addParentComAdminGroupToCheckGenericPropagationTest("findAdminAuthorized"); + } + + @Test + public void addParentComAdminGroupToCheckEditPropagationTest() throws Exception { + addParentComAdminGroupToCheckGenericPropagationTest("findEditAuthorized"); + } + + @Test + public void addParentComAdminGroupToCheckAddPropagationTest() throws Exception { + addParentComAdminGroupToCheckGenericPropagationTest("findAddAuthorized"); + } + + public void addParentComAdminGroupToCheckGenericPropagationTest(String method) throws Exception { + context.turnOffAuthorisationSystem(); + + Community rootCommunity = CommunityBuilder.createCommunity(context) + .withName("Root Community") + .build(); + + Community subCommunity = CommunityBuilder.createSubCommunity(context, rootCommunity) + .withName("MyTestCom") + .build(); + + context.restoreAuthSystemState(); + + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken).perform(get("/api/core/communities/search/" + method) + .param("query", "MyTestCom")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + AtomicReference idRef = new AtomicReference<>(); + ObjectMapper mapper = new ObjectMapper(); + GroupRest groupRest = new GroupRest(); + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(post("/api/core/communities/" + rootCommunity.getID() + "/adminGroup") + .content(mapper.writeValueAsBytes(groupRest)) + .contentType(contentType)) + .andExpect(status().isCreated()) + .andDo(result -> idRef.set( + UUID.fromString(read(result.getResponse().getContentAsString(), "$.id"))) + ); + + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken).perform(post("/api/eperson/groups/" + idRef.get() + "/epersons") + .contentType(parseMediaType(TEXT_URI_LIST_VALUE)) + .content(REST_SERVER_URL + "eperson/groups/" + eperson.getID() + )); + + getClient(epersonToken).perform(get("/api/core/communities/search/" + method) + .param("query", "MyTestCom")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.communities", Matchers.contains(CommunityMatcher + .matchProperties(subCommunity.getName(), + subCommunity.getID(), + subCommunity.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void removeParentComAdminPolicyToCheckAdminPropagationTest() throws Exception { + removeParentComAdminPolicyToCheckGenericPropagationTest("findAdminAuthorized"); + } + + @Test + public void removeParentComAdminPolicyToCheckEditPropagationTest() throws Exception { + removeParentComAdminPolicyToCheckGenericPropagationTest("findEditAuthorized"); + } + + @Test + public void removeParentComAdminPolicyToCheckAddPropagationTest() throws Exception { + removeParentComAdminPolicyToCheckGenericPropagationTest("findAddAuthorized"); + } + + public void removeParentComAdminPolicyToCheckGenericPropagationTest(String method) throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Root Community") + .build(); + + ResourcePolicy policy = ResourcePolicyBuilder.createResourcePolicy(context, eperson, null) + .withDspaceObject(parentCommunity).withAction(Constants.ADMIN) + .build(); + + Community subCommunity = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("MyTestCom") + .build(); + context.restoreAuthSystemState(); + + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken).perform(get("/api/core/communities/search/" + method) + .param("query", "MyTestCom")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.communities", Matchers.contains(CommunityMatcher + .matchProperties(subCommunity.getName(), + subCommunity.getID(), + subCommunity.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(delete("/api/authz/resourcepolicies/" + policy.getID())) + .andExpect(status().is(204)); + + getClient(epersonToken).perform(get("/api/core/communities/search/" + method) + .param("query", "MyTestCom")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + } + + @Test + public void findEditAuthorizedUnauthorizedTest() throws Exception { + getClient().perform(get("/api/core/communities/search/findEditAuthorized")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findEditAuthorizedResourcePolicyTest() throws Exception { + context.turnOffAuthorisationSystem(); + Community comm1 = CommunityBuilder.createCommunity(context).withName("Community 1").build(); + + EPerson hasDirectEditRights = EPersonBuilder.createEPerson(context) + .withEmail("has@editrights.com").withPassword(password) + .build(); + EPerson hasDirectAdminRights = EPersonBuilder.createEPerson(context) + .withEmail("has@adminrights.com").withPassword(password) + .build(); + Community byResourcePolicy = CommunityBuilder.createCommunity(context) + .withName("direct edit rights for eperson").build(); + Community uneditable = CommunityBuilder.createCommunity(context) + .withName("uneditable community") + .build(); + ResourcePolicy policy = ResourcePolicyBuilder.createResourcePolicy(context, hasDirectEditRights, null) + .withDspaceObject(byResourcePolicy).withAction(Constants.WRITE) + .build(); + policy = ResourcePolicyBuilder.createResourcePolicy(context, hasDirectAdminRights, null) + .withDspaceObject(byResourcePolicy).withAction(Constants.ADMIN) + .build(); + context.restoreAuthSystemState(); + + String tokenHasDirectEditRightsToken = getAuthToken(hasDirectEditRights.getEmail(), password); + String tokenHasDirectAdminRightsToken = getAuthToken(hasDirectAdminRights.getEmail(), password); + + getClient(tokenHasDirectEditRightsToken).perform(get("/api/core/communities/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.contains(CommunityMatcher.matchCommunity(byResourcePolicy)))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + getClient(tokenHasDirectAdminRightsToken).perform(get("/api/core/communities/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.contains(CommunityMatcher.matchCommunity(byResourcePolicy)))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void findEditAuthorizedAdminPropagationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + /* + DSO structure: + root + └── subcomm1 + └── subcomm1subcomm2 + */ + EPerson rootAdmin = EPersonBuilder.createEPerson(context) + .withEmail("root@admin.com").withPassword(password).build(); + EPerson subcomm1Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm1@admin.com").withPassword(password).build(); + EPerson subcomm2Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm2@admin.com").withPassword(password).build(); + + Community root = CommunityBuilder.createCommunity(context) + .withAdminGroup(rootAdmin) + .withName("root") + .build(); + Community subcomm1 = CommunityBuilder.createSubCommunity(context, root) + .withAdminGroup(subcomm1Admin) + .withName("subcomm1") + .build(); + Community subcomm1subcomm2 = CommunityBuilder.createSubCommunity(context, subcomm1) + .withAdminGroup(subcomm2Admin) + .withName("subcomm1subcomm2") + .build(); + context.restoreAuthSystemState(); + + String siteAdminToken = getAuthToken(admin.getEmail(), password); + String rootAdminToken = getAuthToken(rootAdmin.getEmail(), password); + String subcomm1AdminToken = getAuthToken(subcomm1Admin.getEmail(), password); + String subcomm2AdminToken = getAuthToken(subcomm2Admin.getEmail(), password); + + getClient(siteAdminToken).perform(get("/api/core/communities/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.containsInAnyOrder( + CommunityMatcher.matchCommunity(root), + CommunityMatcher.matchCommunity(subcomm1), + CommunityMatcher.matchCommunity(subcomm1subcomm2) + ))) + .andExpect(jsonPath("$.page.totalElements", is(3))); + + getClient(rootAdminToken).perform(get("/api/core/communities/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.containsInAnyOrder( + CommunityMatcher.matchCommunity(root), + CommunityMatcher.matchCommunity(subcomm1), + CommunityMatcher.matchCommunity(subcomm1subcomm2) + ))) + .andExpect(jsonPath("$.page.totalElements", is(3))); + + getClient(subcomm1AdminToken).perform(get("/api/core/communities/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.containsInAnyOrder( + CommunityMatcher.matchCommunity(subcomm1), + CommunityMatcher.matchCommunity(subcomm1subcomm2) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + getClient(subcomm2AdminToken).perform(get("/api/core/communities/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.containsInAnyOrder( + CommunityMatcher.matchCommunity(subcomm1subcomm2) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + } + + @Test + public void findAddAuthorizedUnauthorizedTest() throws Exception { + getClient().perform(get("/api/core/communities/search/findAddAuthorized")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findAddAuthorizedResourcePolicyTest() throws Exception { + context.turnOffAuthorisationSystem(); + Community comm1 = CommunityBuilder.createCommunity(context).withName("Community 1").build(); + + EPerson hasDirectEditRights = EPersonBuilder.createEPerson(context) + .withEmail("has@editrights.com").withPassword(password) + .build(); + EPerson hasDirectAdminRights = EPersonBuilder.createEPerson(context) + .withEmail("has@adminrights.com").withPassword(password) + .build(); + Community byResourcePolicy = CommunityBuilder.createCommunity(context) + .withName("direct add rights for eperson").build(); + Community uneditable = CommunityBuilder.createCommunity(context) + .withName("no add community") + .build(); + ResourcePolicy policy = ResourcePolicyBuilder.createResourcePolicy(context, hasDirectEditRights, null) + .withDspaceObject(byResourcePolicy).withAction(Constants.ADD) + .build(); + policy = ResourcePolicyBuilder.createResourcePolicy(context, hasDirectAdminRights, null) + .withDspaceObject(byResourcePolicy).withAction(Constants.ADMIN) + .build(); + context.restoreAuthSystemState(); + + String tokenHasDirectAddRightsToken = getAuthToken(hasDirectEditRights.getEmail(), password); + String tokenHasDirectAdminRightsToken = getAuthToken(hasDirectAdminRights.getEmail(), password); + + getClient(tokenHasDirectAddRightsToken).perform(get("/api/core/communities/search/findAddAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.contains(CommunityMatcher.matchCommunity(byResourcePolicy)))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + getClient(tokenHasDirectAdminRightsToken).perform(get("/api/core/communities/search/findAddAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.contains(CommunityMatcher.matchCommunity(byResourcePolicy)))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void findAddAuthorizedAdminPropagationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + /* + DSO structure: + root + └── subcomm1 + └── subcomm2 + */ + EPerson rootAdmin = EPersonBuilder.createEPerson(context) + .withEmail("root@admin.com").withPassword(password).build(); + EPerson subcomm1Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm1@admin.com").withPassword(password).build(); + EPerson subcomm2Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm2@admin.com").withPassword(password).build(); + + Community root = CommunityBuilder.createCommunity(context) + .withAdminGroup(rootAdmin) + .withName("root") + .build(); + Community subcomm1 = CommunityBuilder.createSubCommunity(context, root) + .withAdminGroup(subcomm1Admin) + .withName("subcomm1") + .build(); + Community subcomm2 = CommunityBuilder.createSubCommunity(context, subcomm1) + .withAdminGroup(subcomm2Admin) + .withName("subcomm2") + .build(); + context.restoreAuthSystemState(); + + String siteAdminToken = getAuthToken(admin.getEmail(), password); + String rootAdminToken = getAuthToken(rootAdmin.getEmail(), password); + String subcomm1AdminToken = getAuthToken(subcomm1Admin.getEmail(), password); + String subcomm2AdminToken = getAuthToken(subcomm2Admin.getEmail(), password); + + getClient(siteAdminToken).perform(get("/api/core/communities/search/findAddAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.containsInAnyOrder( + CommunityMatcher.matchCommunity(root), + CommunityMatcher.matchCommunity(subcomm1), + CommunityMatcher.matchCommunity(subcomm2) + ))) + .andExpect(jsonPath("$.page.totalElements", is(3))); + + getClient(rootAdminToken).perform(get("/api/core/communities/search/findAddAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.containsInAnyOrder( + CommunityMatcher.matchCommunity(root), + CommunityMatcher.matchCommunity(subcomm1), + CommunityMatcher.matchCommunity(subcomm2) + ))) + .andExpect(jsonPath("$.page.totalElements", is(3))); + + getClient(subcomm1AdminToken).perform(get("/api/core/communities/search/findAddAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.containsInAnyOrder( + CommunityMatcher.matchCommunity(subcomm1), + CommunityMatcher.matchCommunity(subcomm2) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + getClient(subcomm2AdminToken).perform(get("/api/core/communities/search/findAddAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.containsInAnyOrder( + CommunityMatcher.matchCommunity(subcomm2) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void findEditAuthorizedCommunitiesWithQueryTest() throws Exception { + findGenericAuthorizedCommunitiesWithQueryTest("findEditAuthorized"); + } + + @Test + public void findAddAuthorizedCommunitiesWithQueryTest() throws Exception { + findGenericAuthorizedCommunitiesWithQueryTest("findAddAuthorized"); + } + + @Test + public void findReadAuthorizedCommunitiesWithQueryTest() throws Exception { + findGenericAuthorizedCommunitiesWithQueryTest("findAdminAuthorized"); + } + + public void findGenericAuthorizedCommunitiesWithQueryTest(String method) throws Exception { + + context.turnOffAuthorisationSystem(); + + EPerson eperson2 = EPersonBuilder.createEPerson(context) + .withEmail("eperson2@mail.com") + .withPassword(password) + .build(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Community child2 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community Two") + .build(); + Community com1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sample community") + .withAdminGroup(eperson) + .build(); + Community com2 = CommunityBuilder.createSubCommunity(context, child1) + .withName("Test community") + .build(); + Community com3 = CommunityBuilder.createSubCommunity(context, child2) + .withName("community of sample items") + .withAdminGroup(eperson) + .build(); + Community com4 = CommunityBuilder.createSubCommunity(context, child2) + .withName("Testing autocomplete in community") + .withAdminGroup(eperson2) + .build(); + Community com5 = CommunityBuilder.createSubCommunity(context, child2) + .withName("Title: subtitle (special characters)") + .build(); + context.restoreAuthSystemState(); + + // Test simple query + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(get("/api/core/communities/search/" + method) + .param("query", "community")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.communities", Matchers.containsInAnyOrder( + CommunityMatcher.matchProperties(com1.getName(), com1.getID(), com1.getHandle()), + CommunityMatcher.matchProperties(com3.getName(), com3.getID(), com3.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test case insensitive query + getClient(tokenEPerson).perform(get("/api/core/communities/search/" + method) + .param("query", "COMMUNITY")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.communities", Matchers.containsInAnyOrder( + CommunityMatcher.matchProperties(com1.getName(), com1.getID(), com1.getHandle()), + CommunityMatcher.matchProperties(com3.getName(), com3.getID(), com3.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test word for unauthorized community + getClient(tokenEPerson).perform(get("/api/core/communities/search/" + method) + .param("query", "test")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + // Test eperson with no authorized communities + getClient(tokenEPerson).perform(get("/api/core/communities/search/" + method) + .param("query", "auto")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + String tokenEPerson2 = getAuthToken(eperson2.getEmail(), password); + // Test eperson2 gets only their authorized community + getClient(tokenEPerson2).perform(get("/api/core/communities/search/" + method) + .param("query", "auto")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.communities", Matchers.contains( + CommunityMatcher.matchProperties(com4.getName(), com4.getID(), com4.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Test query with multiple words + getClient(tokenEPerson2).perform(get("/api/core/communities/search/" + method) + .param("query", "testing auto")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.communities", Matchers.containsInAnyOrder( + CommunityMatcher.matchProperties(com4.getName(), com4.getID(), com4.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Test as admin + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/core/communities/search/" + method) + .param("query", "sample")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.communities", Matchers.containsInAnyOrder( + CommunityMatcher.matchProperties(com1.getName(), com1.getID(), com1.getHandle()), + CommunityMatcher.matchProperties(com3.getName(), com3.getID(), com3.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test more specific unsorted query words + getClient(tokenAdmin).perform(get("/api/core/communities/search/" + method) + .param("query", "items sample")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.communities", Matchers.contains( + CommunityMatcher.matchProperties(com3.getName(), com3.getID(), com3.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Test add retrieve the col not authorized to community admin user + getClient(tokenAdmin).perform(get("/api/core/communities/search/" + method) + .param("query", "test")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.communities", Matchers.containsInAnyOrder( + CommunityMatcher.matchProperties(com2.getName(), com2.getID(), com2.getHandle()), + CommunityMatcher.matchProperties(com4.getName(), com4.getID(), com4.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test query with special characters + getClient(tokenAdmin).perform(get("/api/core/communities/search/" + method) + .param("query", "title: subtitle")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.communities", Matchers.contains( + CommunityMatcher.matchProperties(com5.getName(), com5.getID(), com5.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + } + } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryRestControllerIT.java index a9ab4f0b57a4..f6b9d18feedb 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryRestControllerIT.java @@ -113,6 +113,28 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest List> customSortFields = List.of( ); + /** + * Original value of the discovery.highlights.escape-html property, saved here to restore it after running the + * tests. + */ + boolean escapeHTML; + + @Override + public void setUp() throws Exception { + super.setUp(); + context.turnOffAuthorisationSystem(); + escapeHTML = configurationService.getBooleanProperty("discovery.highlights.escape-html"); + context.restoreAuthSystemState(); + } + + @Override + public void destroy() throws Exception { + context.turnOffAuthorisationSystem(); + configurationService.setProperty("discovery.highlights.escape-html", escapeHTML); + context.restoreAuthSystemState(); + super.destroy(); + } + @Test public void rootDiscoverTest() throws Exception { @@ -7007,4 +7029,59 @@ public void discoverSearchObjectsNOTIFYOutgoingConfigurationTest() throws Except .andExpect(jsonPath("$._links.self.href", containsString("/api/discover/search/objects"))); } + @Test + public void discoverSearchObjectsFirstEscapeHTMLTagsBeforeApplyingHitHighlights() throws Exception { + context.turnOffAuthorisationSystem(); + configurationService.setProperty("discovery.highlights.escape-html", true); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection") + .build(); + + ItemBuilder.createItem(context, col1) + .withTitle("This is a test title") + .build(); + context.restoreAuthSystemState(); + + // This test proves that the HTML tags that are in the original metadata, like test, + // are now escaped and should be returned like <a>test</a> + // Only after this happens should the hit highlights be applied + getClient().perform(get("/api/discover/search/objects") + .param("query", "title")) + .andExpect(status().isOk()) + .andExpect(jsonPath( + "$._embedded.searchResult._embedded.objects[0].hitHighlights['dc.title']", + contains("This is a <a>test</a> title"))); + } + + @Test + public void discoverSearchObjectsDontEscapeHTMLTagsBeforeApplyingHitHighlights() throws Exception { + context.turnOffAuthorisationSystem(); + configurationService.setProperty("discovery.highlights.escape-html", false); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection") + .build(); + + ItemBuilder.createItem(context, col1) + .withTitle("This is a test title") + .build(); + context.restoreAuthSystemState(); + + // This test proves that the HTML tags that are in the original metadata, like test, + // are not escaped and should be returned like test + // Only after this happens should the hit highlights be applied + getClient().perform(get("/api/discover/search/objects") + .param("query", "title")) + .andExpect(status().isOk()) + .andExpect(jsonPath( + "$._embedded.searchResult._embedded.objects[0].hitHighlights['dc.title']", + contains("This is a test title"))); + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java index f54fdc38b60e..cb31454f051e 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java @@ -26,6 +26,8 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; +import static org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST_VALUE; +import static org.springframework.http.MediaType.parseMediaType; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; @@ -55,6 +57,7 @@ import org.dspace.app.rest.matcher.CollectionMatcher; import org.dspace.app.rest.matcher.HalMatcher; import org.dspace.app.rest.matcher.ItemMatcher; +import org.dspace.app.rest.model.GroupRest; import org.dspace.app.rest.model.ItemRest; import org.dspace.app.rest.model.MetadataRest; import org.dspace.app.rest.model.MetadataValueRest; @@ -64,6 +67,7 @@ import org.dspace.app.rest.repository.ItemRestRepository; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.app.rest.test.MetadataPatchSuite; +import org.dspace.authorize.ResourcePolicy; import org.dspace.builder.BitstreamBuilder; import org.dspace.builder.BundleBuilder; import org.dspace.builder.CollectionBuilder; @@ -4758,4 +4762,446 @@ public void findAccessStatusForItemTest() throws Exception { .andExpect(jsonPath("$.status", notNullValue())); } + @Test + public void findEditAuthorizedUnauthorizedTest() throws Exception { + getClient().perform(get("/api/core/items/search/findEditAuthorized")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findEditAuthorizedResourcePolicyTest() throws Exception { + context.turnOffAuthorisationSystem(); + Community comm1 = CommunityBuilder.createCommunity(context).withName("Community 1").build(); + Collection col1 = CollectionBuilder.createCollection(context, comm1).withName("Collection 1").build(); + + EPerson hasDirectEditRights = EPersonBuilder.createEPerson(context) + .withEmail("has@editrights.com").withPassword(password) + .build(); + EPerson hasDirectAdminRights = EPersonBuilder.createEPerson(context) + .withEmail("has@adminrights.com").withPassword(password) + .build(); + Item byResourcePolicy = ItemBuilder.createItem(context, col1) + .withTitle("direct edit rights for eperson") + .build(); + Item uneditable = ItemBuilder.createItem(context, col1) + .withTitle("uneditable item") + .build(); + ResourcePolicy policy = ResourcePolicyBuilder.createResourcePolicy(context, hasDirectEditRights, null) + .withDspaceObject(byResourcePolicy).withAction(WRITE) + .build(); + policy = ResourcePolicyBuilder.createResourcePolicy(context, hasDirectAdminRights, null) + .withDspaceObject(byResourcePolicy).withAction(Constants.ADMIN) + .build(); + context.restoreAuthSystemState(); + + String tokenHasDirectEditRightsToken = getAuthToken(hasDirectEditRights.getEmail(), password); + String tokenHasDirectAdminRightsToken = getAuthToken(hasDirectAdminRights.getEmail(), password); + + getClient(tokenHasDirectEditRightsToken).perform(get("/api/core/items/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.items", + Matchers.contains(ItemMatcher.matchItemProperties(byResourcePolicy)))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + getClient(tokenHasDirectAdminRightsToken).perform(get("/api/core/items/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.items", + Matchers.contains(ItemMatcher.matchItemProperties(byResourcePolicy)))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void findEditAuthorizedAdminPropagationTest() throws Exception { + /* + Cases: + - items in collection with admin rights + - items in collection in community with admin rights + */ + + context.turnOffAuthorisationSystem(); + + /* + DSO structure: + root + ├── subcomm1 + ├── subcomm1collA + ├── subcomm1collAitemX + ├── subcomm1collAitemY + ├── subcomm1collB + └── subcomm1collBitem + └── subcomm2 + └── subcomm2coll + └── subcomm2collitem + */ + EPerson rootAdmin = EPersonBuilder.createEPerson(context) + .withEmail("root@admin.com").withPassword(password).build(); + EPerson subcomm1Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm1@admin.com").withPassword(password).build(); + EPerson subcomm2Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm2@admin.com").withPassword(password).build(); + EPerson subcomm1collA_Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm1collA@admin.com").withPassword(password).build(); + EPerson subcomm1collB_Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm1collB@admin.com").withPassword(password).build(); + EPerson subcomm2collAdmin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm2coll@admin.com").withPassword(password).build(); + + Community root = CommunityBuilder.createCommunity(context) + .withAdminGroup(rootAdmin) + .withName("root") + .build(); + Community subcomm1 = CommunityBuilder.createSubCommunity(context, root) + .withAdminGroup(subcomm1Admin) + .withName("subcomm1") + .build(); + Community subcomm2 = CommunityBuilder.createSubCommunity(context, root) + .withAdminGroup(subcomm2Admin) + .withName("subcomm2") + .build(); + Collection subcomm1collA = CollectionBuilder.createCollection(context, subcomm1) + .withAdminGroup(subcomm1collA_Admin) + .withName("subcomm1collA") + .build(); + Collection subcomm1collB = CollectionBuilder.createCollection(context, subcomm1) + .withAdminGroup(subcomm1collB_Admin) + .withName("subcomm1collB") + .build(); + Collection subcomm2coll = CollectionBuilder.createCollection(context, subcomm2) + .withAdminGroup(subcomm2collAdmin) + .withName("subcomm2coll") + .build(); + Item subcomm1collAitemX = ItemBuilder.createItem(context, subcomm1collA).withTitle("subcomm1collAitemX") + .build(); + Item subcomm1collAitemY = ItemBuilder.createItem(context, subcomm1collA).withTitle("subcomm1collAitemY") + .build(); + Item subcomm1collBitem = ItemBuilder.createItem(context, subcomm1collB).withTitle("subcomm1collBitem") + .build(); + Item subcomm2collitem = ItemBuilder.createItem(context, subcomm2coll).withTitle("subcomm2collitem") + .build(); + context.restoreAuthSystemState(); + + String siteAdminToken = getAuthToken(admin.getEmail(), password); + String rootAdminToken = getAuthToken(rootAdmin.getEmail(), password); + String subcomm1AdminToken = getAuthToken(subcomm1Admin.getEmail(), password); + String subcomm2AdminToken = getAuthToken(subcomm2Admin.getEmail(), password); + String subcomm1collA_AdminToken = getAuthToken(subcomm1collA_Admin.getEmail(), password); + String subcomm1collB_AdminToken = getAuthToken(subcomm1collB_Admin.getEmail(), password); + String subcomm2collAdminToken = getAuthToken(subcomm2collAdmin.getEmail(), password); + + getClient(siteAdminToken).perform(get("/api/core/items/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.items", + Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(subcomm1collAitemX), + ItemMatcher.matchItemProperties(subcomm1collAitemY), + ItemMatcher.matchItemProperties(subcomm1collBitem), + ItemMatcher.matchItemProperties(subcomm2collitem) + ))) + .andExpect(jsonPath("$.page.totalElements", is(4))); + + getClient(rootAdminToken).perform(get("/api/core/items/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.items", + Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(subcomm1collAitemX), + ItemMatcher.matchItemProperties(subcomm1collAitemY), + ItemMatcher.matchItemProperties(subcomm1collBitem), + ItemMatcher.matchItemProperties(subcomm2collitem) + ))) + .andExpect(jsonPath("$.page.totalElements", is(4))); + + getClient(subcomm1AdminToken).perform(get("/api/core/items/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.items", + Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(subcomm1collAitemX), + ItemMatcher.matchItemProperties(subcomm1collAitemY), + ItemMatcher.matchItemProperties(subcomm1collBitem) + ))) + .andExpect(jsonPath("$.page.totalElements", is(3))); + + getClient(subcomm2AdminToken).perform(get("/api/core/items/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.items", + Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(subcomm2collitem) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + getClient(subcomm1collA_AdminToken).perform(get("/api/core/items/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.items", + Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(subcomm1collAitemX), + ItemMatcher.matchItemProperties(subcomm1collAitemY) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + getClient(subcomm1collB_AdminToken).perform(get("/api/core/items/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.items", + Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(subcomm1collBitem) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + getClient(subcomm2collAdminToken).perform(get("/api/core/items/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.items", + Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(subcomm2collitem) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void addParentComAdminGroupToCheckReindexingTest() throws Exception { + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("col1") + .build(); + + Item item = ItemBuilder.createItem(context, col1) + .withTitle("MyTest") + .build(); + + context.restoreAuthSystemState(); + + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken).perform(get("/api/core/items/search/findEditAuthorized") + .param("query", "MyTest")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + AtomicReference idRef = new AtomicReference<>(); + ObjectMapper mapper = new ObjectMapper(); + GroupRest groupRest = new GroupRest(); + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(post("/api/core/communities/" + parentCommunity.getID() + "/adminGroup") + .content(mapper.writeValueAsBytes(groupRest)) + .contentType(contentType)) + .andExpect(status().isCreated()) + .andDo(result -> idRef.set( + UUID.fromString(read(result.getResponse().getContentAsString(), "$.id"))) + ); + + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken).perform(post("/api/eperson/groups/" + idRef.get() + "/epersons") + .contentType(parseMediaType(TEXT_URI_LIST_VALUE)) + .content(REST_SERVER_URL + "eperson/groups/" + eperson.getID() + )); + + getClient(epersonToken).perform(get("/api/core/items/search/findEditAuthorized") + .param("query", "MyTest")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.items", Matchers.contains(ItemMatcher + .matchItemProperties(item) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void removeParentComAdminPolicyToCheckEditPropagationTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + ResourcePolicy policy = ResourcePolicyBuilder.createResourcePolicy(context, eperson, null) + .withDspaceObject(parentCommunity).withAction(Constants.ADMIN) + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("col1") + .build(); + + Item item = ItemBuilder.createItem(context, col1) + .withTitle("MyTest") + .build(); + + context.restoreAuthSystemState(); + + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken).perform(get("/api/core/items/search/findEditAuthorized") + .param("query", "MyTest")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.items", Matchers.contains(ItemMatcher + .matchItemProperties(item) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(delete("/api/authz/resourcepolicies/" + policy.getID())) + .andExpect(status().is(204)); + + getClient(epersonToken).perform(get("/api/core/items/search/findEditAuthorized") + .param("query", "MyTest")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + } + + @Test + public void findEditAuthorizedItemsWithQueryTest() throws Exception { + findGenericAuthorizedItemsWithQueryTest("findEditAuthorized"); + } + + public void findGenericAuthorizedItemsWithQueryTest(String method) throws Exception { + + context.turnOffAuthorisationSystem(); + + EPerson eperson2 = EPersonBuilder.createEPerson(context) + .withEmail("eperson2@mail.com") + .withPassword(password) + .build(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Community child2 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community Two") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Sample collection") + .withAdminGroup(eperson) + .build(); + Collection col2 = CollectionBuilder.createCollection(context, child1) + .withName("col2") + .build(); + Collection col3 = CollectionBuilder.createCollection(context, child2) + .withName("col3") + .withAdminGroup(eperson) + .build(); + Collection col4 = CollectionBuilder.createCollection(context, child2) + .withName("col4") + .withAdminGroup(eperson2) + .build(); + Item item1 = ItemBuilder.createItem(context, col1) + .withTitle("Sample item") + .build(); + Item item2 = ItemBuilder.createItem(context, col2) + .withTitle("Test item") + .build(); + Item item3 = ItemBuilder.createItem(context, col3) + .withTitle("Item of sample bitstreams") + .build(); + Item item4 = ItemBuilder.createItem(context, col4) + .withTitle("Testing autocomplete in items") + .build(); + Item item5 = ItemBuilder.createItem(context, col4) + .withTitle("Title: subtitle (special characters)") + .build(); + + context.restoreAuthSystemState(); + + // Test simple query + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(get("/api/core/items/search/" + method) + .param("query", "item")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.items", Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(item1), + ItemMatcher.matchItemProperties(item3) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test case insensitive + getClient(tokenEPerson).perform(get("/api/core/items/search/" + method) + .param("query", "ITEM")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.items", Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(item1), + ItemMatcher.matchItemProperties(item3) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test word for unauthorized item + getClient(tokenEPerson).perform(get("/api/core/items/search/" + method) + .param("query", "test")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + // Test eperson with no authorized items + getClient(tokenEPerson).perform(get("/api/core/items/search/" + method) + .param("query", "auto")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + String tokenEPerson2 = getAuthToken(eperson2.getEmail(), password); + // Test eperson2 with one authorized item + getClient(tokenEPerson2).perform(get("/api/core/items/search/" + method) + .param("query", "auto")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.items", Matchers.contains( + ItemMatcher.matchItemProperties(item4) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Test query with multiple words + getClient(tokenEPerson2).perform(get("/api/core/items/search/" + method) + .param("query", "testing auto")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.items", Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(item4) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Test query as admin + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/core/items/search/" + method) + .param("query", "sample")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.items", Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(item1), + ItemMatcher.matchItemProperties(item3) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test unsorted query words + getClient(tokenAdmin).perform(get("/api/core/items/search/" + method) + .param("query", "bitstreams sample")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.items", Matchers.contains( + ItemMatcher.matchItemProperties(item3) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Test item not authorized for eperson is returned for admin + getClient(tokenAdmin).perform(get("/api/core/items/search/" + method) + .param("query", "test")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.items", Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(item2), + ItemMatcher.matchItemProperties(item4) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test special characters in query + getClient(tokenAdmin).perform(get("/api/core/items/search/" + method) + .param("query", "title: subtitle (special")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.items", Matchers.contains( + ItemMatcher.matchItemProperties(item5) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + } + } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RequestItemRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RequestItemRepositoryIT.java index 56409d18d738..f1ab2c1c4e3a 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RequestItemRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RequestItemRepositoryIT.java @@ -13,6 +13,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; @@ -57,6 +58,7 @@ import org.dspace.content.Bitstream; import org.dspace.content.Collection; import org.dspace.content.Item; +import org.dspace.content.factory.ContentServiceFactory; import org.dspace.services.ConfigurationService; import org.hamcrest.Matchers; import org.junit.Before; @@ -639,4 +641,83 @@ public void testGetLinkTokenEmailWithoutSubPath() throws MalformedURLException, assertEquals(expectedUrl, generatedLink); configurationService.reloadConfig(); } + + /** + * Test that deleting a bitstream also removes any {@link RequestItem} entities associated with it. + */ + @Test + public void testDeleteBitstreamRemovesRequestItem() throws Exception { + // Fake up a request in REST form. + RequestItemRest rir = new RequestItemRest(); + rir.setAllfiles(false); + rir.setItemId(item.getID().toString()); + rir.setBitstreamId(bitstream.getID().toString()); + rir.setRequestEmail(eperson.getEmail()); + rir.setRequestName(eperson.getFullName()); + rir.setRequestMessage(RequestItemBuilder.REQ_MESSAGE); + + // Create it and see if it was created correctly. + ObjectMapper mapper = new ObjectMapper(); + String authToken = getAuthToken(eperson.getEmail(), password); + + getClient(authToken) + .perform(post(URI_ROOT) + .content(mapper.writeValueAsBytes(rir)) + .contentType(contentType)) + .andExpect(status().isCreated()) + // verify the body is empty + .andExpect(jsonPath("$").doesNotExist()); + + // Verify the request item exists via findByBitstreamId before deletion + Iterator bitstreamRequests = requestItemService.findByBitstreamId(context, bitstream.getID()); + assertTrue("Request item should exist before bitstream deletion", bitstreamRequests.hasNext()); + + // Delete associated Bitstream + ContentServiceFactory.getInstance().getBitstreamService().delete(context, bitstream); + + // Verify that all RequestItems related to this bitstream have been removed + Iterator itemRequests = requestItemService.findByItem(context, item); + assertFalse(itemRequests.hasNext()); + + // Also verify via findByBitstreamId + Iterator remaining = requestItemService.findByBitstreamId(context, bitstream.getID()); + assertFalse("Request items should be removed after bitstream deletion", remaining.hasNext()); + } + + /** + * Test that findByBitstreamId returns matching request items and does not return items for other bitstreams. + */ + @Test + public void testFindByBitstreamId() throws Exception { + context.turnOffAuthorisationSystem(); + + // Create a request item for the existing bitstream + RequestItemBuilder.createRequestItem(context, item, bitstream) + .build(); + + // Create a second bitstream with no request items + InputStream is2 = new ByteArrayInputStream("other content".getBytes()); + Bitstream bitstream2 = BitstreamBuilder + .createBitstream(context, item, is2) + .withName("Other Bitstream") + .build(); + + context.restoreAuthSystemState(); + + // findByBitstreamId should return the request for the first bitstream + Iterator results = requestItemService.findByBitstreamId(context, bitstream.getID()); + assertTrue("Should find request item for bitstream", results.hasNext()); + RequestItem found = results.next(); + assertEquals("Request item should reference correct bitstream", + bitstream.getID(), found.getBitstream().getID()); + assertFalse("Should only find one request item", results.hasNext()); + + // findByBitstreamId should return nothing for the second bitstream + Iterator noResults = requestItemService.findByBitstreamId(context, bitstream2.getID()); + assertFalse("Should find no request items for bitstream without requests", noResults.hasNext()); + + // findByBitstreamId should return nothing for a random UUID + Iterator randomResults = requestItemService.findByBitstreamId(context, UUID.randomUUID()); + assertFalse("Should find no request items for nonexistent bitstream", randomResults.hasNext()); + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java index 493b3bf94628..a0c66cddb70e 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java @@ -144,6 +144,7 @@ public void setUp() throws Exception { user = EPersonBuilder.createEPerson(context) .withEmail("user@example.com") + .withNameInMetadata("Example", "User") .withPassword(password) .build(); @@ -323,7 +324,7 @@ public void testFindByIdWithoutOwnerUser() throws Exception { public void testCreateAndReturn() throws Exception { String id = user.getID().toString(); - String name = user.getName(); + String name = user.getFullName(); String authToken = getAuthToken(user.getEmail(), password); @@ -342,6 +343,8 @@ public void testCreateAndReturn() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.type", is("item"))) .andExpect(jsonPath("$.metadata", matchMetadata("dspace.object.owner", name, id, 0))) + .andExpect(jsonPath("$.metadata", matchMetadata("person.givenName", user.getFirstName(), 0))) + .andExpect(jsonPath("$.metadata", matchMetadata("person.familyName", user.getLastName(), 0))) .andExpect(jsonPath("$.metadata", matchMetadata("dspace.entity.type", "Person", 0))); getClient(authToken).perform(get("/api/eperson/profiles/{id}/eperson", id)) @@ -391,7 +394,7 @@ public void testCreateAndReturnWithPublicProfile() throws Exception { public void testCreateAndReturnWithAdmin() throws Exception { String id = user.getID().toString(); - String name = user.getName(); + String name = user.getFullName(); configurationService.setProperty("researcher-profile.collection.uuid", null); @@ -412,6 +415,8 @@ public void testCreateAndReturnWithAdmin() throws Exception { getClient(authToken).perform(get("/api/eperson/profiles/{id}/item", id)) .andExpect(status().isOk()) .andExpect(jsonPath("$.type", is("item"))) + .andExpect(jsonPath("$.metadata", matchMetadata("person.givenName", user.getFirstName(), 0))) + .andExpect(jsonPath("$.metadata", matchMetadata("person.familyName", user.getLastName(), 0))) .andExpect(jsonPath("$.metadata", matchMetadata("dspace.object.owner", name, id, 0))) .andExpect(jsonPath("$.metadata", matchMetadata("dspace.entity.type", "Person", 0))); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java index 9a8d14f3d658..84236fd58fe8 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java @@ -45,6 +45,7 @@ public void tesGetRecords() throws Exception { CloseableHttpClient originalHttpClient = liveImportClient.getHttpClient(); CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + //ror-records.json is the result of a GET request to https://api.ror.org/v2/organizations at 16/07/2025. try (InputStream file = getClass().getResourceAsStream("ror-records.json")) { String jsonResponse = IOUtils.toString(file, Charset.defaultCharset()); @@ -59,21 +60,19 @@ public void tesGetRecords() throws Exception { ImportRecord record = recordsImported.iterator().next(); - assertThat(record.getValueList(), hasSize(11)); + assertThat(record.getValueList(), hasSize(9)); + + assertThat(record.getSingleValue("organization.legalName"), + is("University American College Skopje")); + assertThat(record.getSingleValue("organization.identifier.ror"), is("https://ror.org/05hknds03")); + assertThat(record.getSingleValue("organization.alternateName"), is("UACS")); + assertThat(record.getSingleValue("organization.url"), is("https://uacs.edu.mk")); + assertThat(record.getSingleValue("dc.type"), is("education")); + assertThat(record.getSingleValue("organization.address.addressCountry"), is("MK")); + assertThat(record.getSingleValue("organization.address.addressLocality"), is("Skopje")); + assertThat(record.getSingleValue("organization.foundingDate"), is("2005")); + assertThat(record.getSingleValue("organization.identifier.isni"), is("0000 0004 0446 4427")); - assertThat( - record.getSingleValue("organization.legalName"), - is("The University of Texas") - ); - assertThat(record.getSingleValue("organization.identifier.ror"), is("https://ror.org/02f6dcw23")); - assertThat(record.getSingleValue("organization.alternateName"), is("UTHSCSA")); - assertThat(record.getSingleValue("organization.url"), is("http://www.uthscsa.edu/")); - assertThat(record.getSingleValue("dc.type"), is("Education")); - assertThat(record.getSingleValue("organization.address.addressCountry"), is("US")); - assertThat(record.getSingleValue("organization.foundingDate"), is("1959")); - assertThat(record.getValue("organization", "identifier", "crossrefid"), hasSize(2)); - assertThat(record.getSingleValue("organization.identifier.isni"), is("0000 0001 0629 5880")); - assertThat(record.getSingleValue("organization.parentOrganization"), is("The University of Texas System")); } finally { liveImportClient.setHttpClient(originalHttpClient); @@ -96,7 +95,7 @@ public void tesCount() throws Exception { context.restoreAuthSystemState(); Integer count = rorServiceImpl.count("test"); - assertThat(count, equalTo(200)); + assertThat(count, equalTo(115409)); } finally { liveImportClient.setHttpClient(originalHttpClient); } @@ -110,6 +109,8 @@ public void tesGetRecord() throws Exception { try (InputStream file = getClass().getResourceAsStream("ror-record.json")) { + System.out.println("file = " + file.toString()); + String jsonResponse = IOUtils.toString(file, Charset.defaultCharset()); liveImportClient.setHttpClient(httpClient); @@ -117,22 +118,20 @@ public void tesGetRecord() throws Exception { when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); context.restoreAuthSystemState(); - ImportRecord record = rorServiceImpl.getRecord("https://ror.org/01sps7q28"); - assertThat(record.getValueList(), hasSize(9)); - assertThat( - record.getSingleValue("organization.legalName"), - is("The University of Texas Health Science Center at Tyler") - ); - assertThat(record.getSingleValue("organization.identifier.ror"), is("https://ror.org/01sps7q28")); - assertThat(record.getSingleValue("organization.alternateName"), is("UTHSCT")); - assertThat(record.getSingleValue("organization.url"), - is("https://www.utsystem.edu/institutions/university-texas-health-science-center-tyler")); - assertThat(record.getSingleValue("dc.type"), is("Healthcare")); + ImportRecord record = rorServiceImpl.getRecord("https://ror.org/02437s643"); + + assertThat(record.getValueList(), hasSize(10)); + assertThat(record.getSingleValue("organization.legalName"), + is("University of Illinois Chicago, Rockford campus")); + assertThat(record.getSingleValue("organization.identifier.ror"), is("https://ror.org/02437s643")); + assertThat(record.getSingleValue("organization.alternateName"), is("UICOMR")); + assertThat(record.getSingleValue("organization.url"), is("https://www.uillinois.edu")); + assertThat(record.getSingleValue("dc.type"), is("education")); assertThat(record.getSingleValue("organization.address.addressCountry"), is("US")); - assertThat(record.getSingleValue("organization.foundingDate"), is("1947")); - assertThat(record.getSingleValue("organization.identifier.isni"), is("0000 0000 9704 5790")); - assertThat(record.getSingleValue("organization.parentOrganization"), is("The University of Texas System")); - + assertThat(record.getSingleValue("organization.address.addressLocality"), is("Rockford")); + assertThat(record.getSingleValue("organization.foundingDate"), is("1972")); + assertThat(record.getSingleValue("organization.identifier.isni"), is("0000 0000 9018 7542")); + assertThat(record.getSingleValue("organization.parentOrganization"), is("University of Illinois Chicago")); } finally { liveImportClient.setHttpClient(originalHttpClient); } @@ -152,7 +151,7 @@ public void tesGetRecordsCount() throws Exception { context.restoreAuthSystemState(); int tot = rorServiceImpl.getRecordsCount("test query"); - assertEquals(200, tot); + assertEquals(115409, tot); } finally { liveImportClient.setHttpClient(originalHttpClient); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SitemapRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SitemapRestControllerIT.java index 681e9bf16b04..084b8272bd09 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SitemapRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SitemapRestControllerIT.java @@ -134,7 +134,7 @@ public void testSitemap_fileSystemTraversal_dspaceCfg() throws Exception { //** WHEN ** //We attempt to use endpoint for malicious file system traversal getClient().perform(get("/" + SITEMAPS_ENDPOINT + "/%2e%2e/config/dspace.cfg")) - .andExpect(status().isBadRequest()); + .andExpect(status().isForbidden()); } @Test @@ -142,7 +142,7 @@ public void testSitemap_fileSystemTraversal_dspaceCfg2() throws Exception { //** WHEN ** //We attempt to use endpoint for malicious file system traversal getClient().perform(get("/" + SITEMAPS_ENDPOINT + "/%2e%2e%2fconfig%2fdspace.cfg")) - .andExpect(status().isBadRequest()); + .andExpect(status().isForbidden()); } @Test diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java index bcea11cbb5a4..b8156396d309 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java @@ -191,8 +191,7 @@ public void findCollections() throws Exception { //Match only that a section exists with a submission configuration behind getClient(token).perform(get("/api/config/submissiondefinitions/traditional/collections") .param("projection", "full")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.page.totalElements", is(0))); + .andExpect(status().isNoContent()); } @Test diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java index ef475353b93e..a16b20605be8 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java @@ -913,6 +913,7 @@ public void unvalidCreateWorkflowItemTest() throws Exception { * * @throws Exception */ + @Test public void validationErrorsRequiredMetadataTest() throws Exception { context.turnOffAuthorisationSystem(); @@ -936,6 +937,7 @@ public void validationErrorsRequiredMetadataTest() throws Exception { XmlWorkflowItem witem = WorkflowItemBuilder.createWorkflowItem(context, col1) .withTitle("Workflow Item 1") .withIssueDate("2017-10-17") + .grantLicense() .build(); //4. a workflow item without the dateissued required field @@ -947,12 +949,12 @@ public void validationErrorsRequiredMetadataTest() throws Exception { String authToken = getAuthToken(eperson.getEmail(), password); - getClient(authToken).perform(get("/api/workflow/worfklowitems/" + witem.getID())) + getClient(authToken).perform(get("/api/workflow/workflowitems/" + witem.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$.errors").doesNotExist()) ; - getClient(authToken).perform(get("/api/workflow/worfklowitems/" + witemMissingFields.getID())) + getClient(authToken).perform(get("/api/workflow/workflowitems/" + witemMissingFields.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$.errors[?(@.message=='error.validation.required')]", Matchers.contains( diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java index 542688ea2396..24a389dcaf69 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java @@ -2618,6 +2618,7 @@ public void patchUpdateMetadataForbiddenTest() throws Exception { (witem, "Workspace Item 1", "2019-01-01", "ExtraEntry")))); } + @Test public void patchReplaceMetadataOnItemStillInSubmissionTest() throws Exception { context.turnOffAuthorisationSystem(); @@ -5929,6 +5930,7 @@ public void patchBitstreamWithAccessConditionLeaseMissingDate() throws Exception .andExpect(jsonPath("$.sections.upload.files[0].accessConditions", empty())); } + @Test public void deleteWorkspaceItemWithMinRelationshipsTest() throws Exception { context.turnOffAuthorisationSystem(); @@ -7983,6 +7985,7 @@ public void createItemWithoutEntityTypeIfCollectionHasBlankEntityType() throws E ))); } + @Test public void verifyBitstreamPolicyNotDuplicatedTest() throws Exception { context.turnOffAuthorisationSystem(); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/RequestCopyFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/RequestCopyFeatureIT.java index 6fd5fad35c69..afae147515aa 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/RequestCopyFeatureIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/RequestCopyFeatureIT.java @@ -457,6 +457,7 @@ public void requestCopyOnBitstreamFromCollectionAsEperson() throws Exception { .andExpect(jsonPath("$._embedded").doesNotExist()); } + @Test public void requestACopyItemTypeLoggedAsAnonymous() throws Exception { configurationService.setProperty("request.item.type", "logged"); @@ -491,6 +492,7 @@ public void requestACopyItemTypeLoggedAsEperson() throws Exception { ); } + @Test public void requestACopyItemTypeEmptyAsAnonymous() throws Exception { configurationService.setProperty("request.item.type", ""); @@ -505,6 +507,7 @@ public void requestACopyItemTypeEmptyAsAnonymous() throws Exception { .andExpect(jsonPath("$._embedded").doesNotExist()); } + @Test public void requestACopyItemTypeEmptyAsEperson() throws Exception { configurationService.setProperty("request.item.type", ""); @@ -521,6 +524,7 @@ public void requestACopyItemTypeEmptyAsEperson() throws Exception { .andExpect(jsonPath("$._embedded").doesNotExist()); } + @Test public void requestACopyItemTypeBogusValueAsAnonymous() throws Exception { configurationService.setProperty("request.item.type", "invalid value"); @@ -535,6 +539,7 @@ public void requestACopyItemTypeBogusValueAsAnonymous() throws Exception { .andExpect(jsonPath("$._embedded").doesNotExist()); } + @Test public void requestACopyItemTypeBogusValueAsEperson() throws Exception { configurationService.setProperty("request.item.type", "invalid value"); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubmissionDefinitionsMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubmissionDefinitionsMatcher.java index 398097db6fba..14d8e5143815 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubmissionDefinitionsMatcher.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubmissionDefinitionsMatcher.java @@ -34,7 +34,7 @@ public static Matcher matchSubmissionDefinition(boolean isDefault, Strin */ public static Matcher matchFullEmbeds() { return matchEmbeds( - "collections[]", + "collections", "sections" ); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/repository/ProcessRestRepositoryTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/repository/ProcessRestRepositoryTest.java new file mode 100644 index 000000000000..210c12ff8a89 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/repository/ProcessRestRepositoryTest.java @@ -0,0 +1,35 @@ +/** + * 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.repository; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.lang.reflect.Method; + +import jakarta.annotation.PostConstruct; +import org.junit.Test; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; + +/** + * Unit tests for {@link ProcessRestRepository}. + */ +public class ProcessRestRepositoryTest { + + @Test + public void testInitRunsAfterApplicationReady() throws Exception { + Method init = ProcessRestRepository.class.getMethod("init"); + EventListener eventListener = init.getAnnotation(EventListener.class); + + assertNotNull(eventListener); + assertArrayEquals(new Class[] { ApplicationReadyEvent.class }, eventListener.value()); + assertNull(init.getAnnotation(PostConstruct.class)); + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandlerTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandlerTest.java index d5a72ea9a5cd..3bc6d1376488 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandlerTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandlerTest.java @@ -76,8 +76,7 @@ public void testJWTEncrypted() throws Exception { //temporary set a negative expiration time so the token is invalid immediately @Test public void testExpiredToken() throws Exception { - when(configurationService.getLongProperty("jwt.shortLived.token.expiration", 1800000)) - .thenReturn(-99999999L); + when(shortLivedJWTTokenHandler.getExpirationPeriod()).thenReturn(-99999999L); when(ePersonClaimProvider.getEPerson(any(Context.class), any(JWTClaimsSet.class))).thenReturn(ePerson); Date previous = new Date(new Date().getTime() - 10000000000L); String token = shortLivedJWTTokenHandler diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/signposting/controller/LinksetRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/signposting/controller/LinksetRestControllerIT.java index cf62d5ac0861..50b7dbdc6533 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/signposting/controller/LinksetRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/signposting/controller/LinksetRestControllerIT.java @@ -8,6 +8,7 @@ package org.dspace.app.rest.signposting.controller; import static org.dspace.content.MetadataSchemaEnum.PERSON; +import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; @@ -19,7 +20,9 @@ import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.time.Period; +import java.util.ArrayList; import java.util.Date; +import java.util.UUID; import org.apache.commons.codec.CharEncoding; import org.apache.commons.io.IOUtils; @@ -692,6 +695,61 @@ public void findTypedLinkForItemWithAuthor() throws Exception { "&& @.type == 'application/linkset+json')]").exists()); } + @Test + public void showTypedLinksMissingForItemWithMoreBitstreamsThanLimit() throws Exception { + String bitstreamContent = "ThisIsSomeDummyText"; + String bitstreamMimeType = "text/plain"; + + int itemBitstreamsLimit = configurationService.getIntProperty("signposting.item.bitstreams.limit", 10); + + context.turnOffAuthorisationSystem(); + Item item = ItemBuilder.createItem(context, collection) + .withTitle("Item Test") + .withMetadata("dc", "identifier", "doi", doi) + .build(); + + // Add more bitstreams than the configured limit + ArrayList bitstreamIDs = new ArrayList<>(); + for (int i = 0; i <= itemBitstreamsLimit; i++) { + Bitstream bitstream = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { + bitstream = BitstreamBuilder.createBitstream(context, item, is) + .withName("Bitstream " + i) + .withDescription("description") + .withMimeType(bitstreamMimeType) + .build(); + + if (bitstream != null) { + bitstreamIDs.add(bitstream.getID()); + } + } + } + context.restoreAuthSystemState(); + + // Make sure the bitstreams were successfully added. + assertTrue("There was a problem ingesting bitstreams.", bitstreamIDs.size() > itemBitstreamsLimit); + + String url = configurationService.getProperty("dspace.ui.url"); + String signpostingUrl = configurationService.getProperty("signposting.path"); + + // There should be typed links to the Link Sets but no typed links to the Bitstreams in the response. + // We only need to check for one of the Bitstream UUIDs, since all of them should be absent. + UUID firstBitstreamId = bitstreamIDs.get(0); + getClient().perform(get("/signposting/links/" + item.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[?(@.href == '" + url + "/" + signpostingUrl + "/linksets/" + + item.getID().toString() + "' " + + "&& @.rel == 'linkset' " + + "&& @.type == 'application/linkset')]").exists()) + .andExpect(jsonPath("$[?(@.href == '" + url + "/" + signpostingUrl + "/linksets/" + + item.getID().toString() + "/json' " + + "&& @.rel == 'linkset' " + + "&& @.type == 'application/linkset+json')]").exists()) + .andExpect(jsonPath("$[?(@.href == '" + url + "/bitstreams/" + firstBitstreamId + "/download' " + + "&& @.rel == 'item' " + + "&& @.type == 'text/plain')]").doesNotExist());; + } + @Test public void findTypedLinkForBitstream() throws Exception { String bitstreamContent = "ThisIsSomeDummyText"; diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/WebappLoggingIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/WebappLoggingIT.java new file mode 100644 index 000000000000..fe746452c792 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/WebappLoggingIT.java @@ -0,0 +1,101 @@ +/** + * 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.test; + +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.LoggerConfig; +import org.apache.logging.log4j.core.config.Property; +import org.apache.logging.log4j.core.layout.PatternLayout; +import org.junit.After; +import org.junit.Test; + +/** + * Test basic log4j logging functionality, extending AbstractControllerIntegrationTest + * purely to make sure we are testing the *web application* and not just the kernel + * as that is where logging has broken in the past. + * + * @author Kim Shepherd + */ +public class WebappLoggingIT extends AbstractControllerIntegrationTest { + + private static final Logger logger = LogManager.getLogger(WebappLoggingIT.class); + private static final String APPENDER_NAME = "DSpaceTestAppender"; + + static class InMemoryAppender extends AbstractAppender { + private final List messages = new ArrayList<>(); + + protected InMemoryAppender(String name) { + super( + name, + null, + PatternLayout.newBuilder().withPattern("%m").build(), + false, + Property.EMPTY_ARRAY + ); + start(); + } + + @Override + public void append(LogEvent event) { + messages.add(event.getMessage().getFormattedMessage()); + } + + public List getMessages() { + return messages; + } + } + + @Test + public void testLogging() throws Exception { + LoggerContext context = (LoggerContext) LogManager.getContext(false); + Configuration config = context.getConfiguration(); + + InMemoryAppender appender = new InMemoryAppender(APPENDER_NAME); + config.addAppender(appender); + + LoggerConfig testLoggerConfig = new LoggerConfig(logger.getName(), Level.INFO, false); + testLoggerConfig.addAppender(appender, null, null); + config.addLogger(logger.getName(), testLoggerConfig); + context.updateLoggers(); + + logger.info("DSPACE TEST LOG ENTRY"); + + List messages = appender.getMessages(); + assertTrue(messages.stream().anyMatch(msg -> msg.contains("DSPACE TEST LOG ENTRY"))); + } + + @After + public void cleanupAppender() { + LoggerContext context = (LoggerContext) LogManager.getContext(false); + Configuration config = context.getConfiguration(); + + config.removeLogger(logger.getName()); + + Appender appender = config.getAppender(APPENDER_NAME); + if (appender != null) { + appender.stop(); + config.getAppenders().remove(APPENDER_NAME); + } + + context.updateLoggers(); +} + +} + diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java b/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java index 24244e1773e6..0b866659edd7 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java @@ -10,16 +10,30 @@ import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; +import java.nio.file.Path; +import java.util.List; + import org.dspace.app.rest.test.AbstractWebClientIntegrationTest; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.content.Collection; import org.dspace.services.ConfigurationService; +import org.hamcrest.MatcherAssert; import org.junit.Assume; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.test.context.TestPropertySource; @@ -45,6 +59,9 @@ public class Swordv1IT extends AbstractWebClientIntegrationTest { private final String DEPOSIT_PATH = "/sword/deposit"; private final String MEDIA_LINK_PATH = "/sword/media-link"; + // ATOM Content type returned by SWORDv1 + private final String ATOM_CONTENT_TYPE = "application/atom+xml;charset=UTF-8"; + @Before public void onlyRunIfConfigExists() { // These integration tests REQUIRE that SWORDWebConfig is found/available (as this class deploys SWORD) @@ -93,10 +110,76 @@ public void depositUnauthorizedTest() throws Exception { } @Test - @Ignore public void depositTest() throws Exception { - // TODO: Actually test a full deposit via SWORD. - // Currently, we are just ensuring the /deposit endpoint exists (see above) and isn't throwing a 404 + context.turnOffAuthorisationSystem(); + // Create a top level community and one Collection + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + // Make sure our Collection allows the "eperson" user to submit into it + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Test SWORDv1 Collection") + .withSubmitterGroup(eperson) + .build(); + // Above changes MUST be committed to the database for SWORDv2 to see them. + context.commit(); + context.restoreAuthSystemState(); + + // Specify zip file + // NOTE: We are using the same "example.zip" as SWORDv2IT because that same ZIP is valid for both v1 and v2 + FileSystemResource zipFile = new FileSystemResource(Path.of("src", "test", "resources", "org", + "dspace", "app", "sword2", "example.zip")); + + // Add required headers + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.valueOf("application/zip")); + headers.setContentDisposition(ContentDisposition.attachment().filename("example.zip").build()); + headers.set("X-Packaging", "http://purl.org/net/sword-types/METSDSpaceSIP"); + headers.setAccept(List.of(MediaType.APPLICATION_ATOM_XML)); + + //---- + // STEP 1: Verify upload/submit via SWORDv1 works + //---- + // Send POST to upload Zip file via SWORD + ResponseEntity response = postResponseAsString(DEPOSIT_PATH + "/" + collection.getHandle(), + eperson.getEmail(), password, + new HttpEntity<>(zipFile.getContentAsByteArray(), + headers)); + + // Expect a 201 CREATED response with ATOM content returned + assertEquals(HttpStatus.CREATED, response.getStatusCode()); + assertEquals(ATOM_CONTENT_TYPE, response.getHeaders().getContentType().toString()); + + // MUST return a "Location" header which is the "/sword/media-link/*" URI of the zip file bitstream within + // the created item (e.g. /sword/media-link/[handle-prefix]/[handle-suffix]/bitstream/[uuid]) + assertNotNull(response.getHeaders().getLocation()); + String mediaLink = response.getHeaders().getLocation().toString(); + + // Body should include the SWORD version in generator tag + MatcherAssert.assertThat(response.getBody(), + containsString("")); + // Verify Item title also is returned in the body + MatcherAssert.assertThat(response.getBody(), containsString("Attempts to detect retrotransposition")); + + //---- + // STEP 2: Verify /media-link access works + //---- + // Media-Link URI should work when requested by the EPerson who did the deposit + HttpHeaders authHeaders = new HttpHeaders(); + authHeaders.setBasicAuth(eperson.getEmail(), password); + RequestEntity request = RequestEntity.get(mediaLink) + .accept(MediaType.valueOf("application/atom+xml")) + .headers(authHeaders) + .build(); + response = responseAsString(request); + + // Expect a 200 response with ATOM feed content returned + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(ATOM_CONTENT_TYPE, response.getHeaders().getContentType().toString()); + // Body should include a link to the zip bitstream in the newly created Item + // This just verifies "example.zip" exists in the body. + MatcherAssert.assertThat(response.getBody(), containsString("example.zip")); } @Test @@ -105,13 +188,8 @@ public void mediaLinkUnauthorizedTest() throws Exception { ResponseEntity response = getResponseAsString(MEDIA_LINK_PATH); // Expect a 401 response code assertThat(response.getStatusCode(), equalTo(HttpStatus.UNAUTHORIZED)); - } - @Test - @Ignore - public void mediaLinkTest() throws Exception { - // TODO: Actually test a /media-link request. - // Currently, we are just ensuring the /media-link endpoint exists (see above) and isn't throwing a 404 + //NOTE: An authorized /media-link test is performed in depositTest() above. } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/sword2/Swordv2IT.java b/dspace-server-webapp/src/test/java/org/dspace/app/sword2/Swordv2IT.java index 296bef1bce4d..f159c7d1c497 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/sword2/Swordv2IT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/sword2/Swordv2IT.java @@ -190,6 +190,48 @@ public void mediaResourceUnauthorizedTest() throws Exception { assertEquals(response.getStatusCode(), HttpStatus.UNAUTHORIZED); } + /** + * There should not be any Internal Server/Authorization error when uploading a new Item with embargo + * The embargo is defined in the `mets.xml` of the `example-embargo.zip` file + */ + @Test + public void depositItemWithEmbargo() throws Exception { + context.turnOffAuthorisationSystem(); + // Create a top level community and one Collection + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + // Make sure our Collection allows the "eperson" user to submit into it + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Test SWORDv2 Collection") + .withSubmitterGroup(eperson) + .build(); + // Above changes MUST be committed to the database for SWORDv2 to see them. + context.commit(); + context.restoreAuthSystemState(); + + // Add file + LinkedMultiValueMap multipart = new LinkedMultiValueMap<>(); + multipart.add("file", new FileSystemResource(Path.of("src", "test", "resources", + "org", "dspace", "app", "sword2", "example-embargo.zip"))); + // Add required headers + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + headers.setContentDisposition(ContentDisposition.attachment().filename("example-embargo.zip").build()); + headers.set("Packaging", "http://purl.org/net/sword/package/METSDSpaceSIP"); + headers.setAccept(List.of(MediaType.APPLICATION_ATOM_XML)); + + + // Send POST to upload Zip file via SWORD + ResponseEntity response = postResponseAsString(COLLECTION_PATH + "/" + collection.getHandle(), + eperson.getEmail(), password, + new HttpEntity<>(multipart, headers)); + + // Expect a 201 CREATED response with ATOM "entry" content returned + assertEquals(HttpStatus.CREATED, response.getStatusCode()); + assertEquals(ATOM_ENTRY_CONTENT_TYPE, response.getHeaders().getContentType().toString()); + } + /** * This tests four different SWORDv2 actions, as these all require starting with a new deposit. * 1. Depositing a new item via SWORD (via POST /collections/[collection-uuid]) @@ -220,7 +262,8 @@ public void depositAndEditViaSwordTest() throws Exception { // Add required headers HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.MULTIPART_FORM_DATA); - headers.setContentDisposition(ContentDisposition.attachment().filename("example.zip").build()); + // Test the file with spaces or special characters in the name + headers.setContentDisposition(ContentDisposition.attachment().filename("example .zip").build()); headers.set("Packaging", "http://purl.org/net/sword/package/METSDSpaceSIP"); headers.setAccept(List.of(MediaType.APPLICATION_ATOM_XML)); diff --git a/dspace-server-webapp/src/test/java/org/dspace/curate/CurationScriptIT.java b/dspace-server-webapp/src/test/java/org/dspace/curate/CurationScriptIT.java index 8c0744a09cce..347ed0935f05 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/curate/CurationScriptIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/curate/CurationScriptIT.java @@ -49,6 +49,7 @@ import org.dspace.scripts.configuration.ScriptConfiguration; import org.dspace.scripts.factory.ScriptServiceFactory; import org.dspace.scripts.service.ScriptService; +import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -214,6 +215,7 @@ public void curateScript_InvalidScope() throws Exception { .andExpect(status().isBadRequest()); } + @Ignore @Test public void curateScript_InvalidTaskFile() throws Exception { String token = getAuthToken(admin.getEmail(), password); @@ -286,6 +288,7 @@ public void curateScript_validRequest_Task() throws Exception { } } + @Ignore @Test public void curateScript_validRequest_TaskFile() throws Exception { context.turnOffAuthorisationSystem(); @@ -667,7 +670,7 @@ public void testURLRedirectCurateTest() throws Exception { // MetadataValueLinkChecker uri field with regular link .withMetadata("dc", "description", null, "https://google.com") // MetadataValueLinkChecker uri field with redirect link - .withMetadata("dc", "description", "uri", "https://demo7.dspace.org/handle/123456789/1") + .withMetadata("dc", "description", "uri", "http://google.com") // MetadataValueLinkChecker uri field with non resolving link .withMetadata("dc", "description", "uri", "https://www.atmire.com/broken-link") .withSubject("ExtraEntry") @@ -690,9 +693,9 @@ public void testURLRedirectCurateTest() throws Exception { // field that should be ignored assertFalse(checkIfInfoTextLoggedByHandler(handler, "demo.dspace.org/home")); - // redirect links in field that should not be ignored (https) => expect OK - assertTrue(checkIfInfoTextLoggedByHandler(handler, "https://demo7.dspace.org/handle/123456789/1 = 200 - OK")); - // regular link in field that should not be ignored (http) => expect OK + // redirect links in field that should not be ignored => expect OK (even though curl responds with 301) + assertTrue(checkIfInfoTextLoggedByHandler(handler, "http://google.com = 200 - OK")); + // regular link in field that should not be ignored => expect OK assertTrue(checkIfInfoTextLoggedByHandler(handler, "https://google.com = 200 - OK")); // nonexistent link in field that should not be ignored => expect 404 assertTrue(checkIfInfoTextLoggedByHandler(handler, "https://www.atmire.com/broken-link = 404 - FAILED")); diff --git a/dspace-server-webapp/src/test/resources/application-test.properties b/dspace-server-webapp/src/test/resources/application-test.properties index bd9e2ea4a17b..ca065187d46c 100644 --- a/dspace-server-webapp/src/test/resources/application-test.properties +++ b/dspace-server-webapp/src/test/resources/application-test.properties @@ -16,5 +16,8 @@ ## This file is found on classpath at src/test/resources/log4j2-test.xml logging.config = classpath:log4j2-test.xml +# Disable LDAP Health Check during tests to avoid external LDAP requirement +management.health.ldap.enabled=false + # Our integration tests expect application to be deployed at the root path (/) -server.servlet.context-path=/ \ No newline at end of file +server.servlet.context-path=/ diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-record.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-record.json index 4d0cd97fd5b6..50df107f7d97 100644 --- a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-record.json +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-record.json @@ -1,107 +1,93 @@ { - "id": "https://ror.org/01sps7q28", - "name": "The University of Texas Health Science Center at Tyler", - "email_address": null, - "ip_addresses": [ - - ], - "established": 1947, - "types": [ - "Healthcare" + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } + }, + "domains": [], + "established": 1972, + "external_ids": [ + { + "all": [ + "grid.430864.d" + ], + "preferred": "grid.430864.d", + "type": "grid" + }, + { + "all": [ + "0000 0000 9018 7542" + ], + "preferred": null, + "type": "isni" + } ], - "relationships": [ + "id": "https://ror.org/02437s643", + "links": [ { - "label": "The University of Texas System", - "type": "Parent", - "id": "https://ror.org/01gek1696" + "type": "website", + "value": "https://www.uillinois.edu" } ], - "addresses": [ + "locations": [ { - "lat": 32.426014, - "lng": -95.212728, - "state": "Texas", - "state_code": "US-TX", - "city": "Tyler", - "geonames_city": { - "id": 4738214, - "city": "Tyler", - "geonames_admin1": { - "name": "Texas", - "id": 4736286, - "ascii_name": "Texas", - "code": "US.TX" - }, - "geonames_admin2": { - "name": "Smith County", - "id": 4729130, - "ascii_name": "Smith County", - "code": "US.TX.423" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "NA", + "continent_name": "North America", + "country_code": "US", + "country_name": "United States", + "country_subdivision_code": "IL", + "country_subdivision_name": "Illinois", + "lat": 42.27113, + "lng": -89.094, + "name": "Rockford" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 4907959 } ], - "links": [ - "https://www.utsystem.edu/institutions/university-texas-health-science-center-tyler" - ], - "aliases": [ - "East Texas Tuberculosis Sanitarium", - "UT Health Northeast" - ], - "acronyms": [ - "UTHSCT" - ], - "status": "active", - "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Texas_Health_Science_Center_at_Tyler", - "labels": [ - - ], - "country": { - "country_name": "United States", - "country_code": "US" - }, - "external_ids": { - "ISNI": { - "preferred": null, - "all": [ - "0000 0000 9704 5790" - ] + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "UICOMR" }, - "OrgRef": { - "preferred": null, - "all": [ - "3446655" - ] + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "University of Illinois Chicago, Rockford campus" }, - "Wikidata": { - "preferred": null, - "all": [ - "Q7896437" - ] + { + "lang": "en", + "types": [ + "alias" + ], + "value": "University of Illinois at Rockford" + } + ], + "relationships": [ + { + "label": "University of Illinois Chicago", + "type": "parent", + "id": "https://ror.org/02mpq6x41" }, - "GRID": { - "preferred": "grid.267310.1", - "all": "grid.267310.1" + { + "label": "Swedish American Hospital", + "type": "related", + "id": "https://ror.org/05scd7d31" } - } -} + ], + "status": "active", + "types": [ + "education" + ] +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-records.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-records.json index 5f93bb7d07a0..46ffbbe9b844 100644 --- a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-records.json +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-records.json @@ -1,2383 +1,1986 @@ { - "number_of_results": 200, - "time_taken": 12, + "number_of_results": 115409, + "time_taken": 63, "items": [ { - "id": "https://ror.org/02f6dcw23", - "name": "The University of Texas", - "email_address": null, - "ip_addresses": [ - - ], - "established": 1959, - "types": [ - "Education" - ], - "relationships": [ - { - "label": "Audie L. Murphy Memorial VA Hospital", - "type": "Related", - "id": "https://ror.org/035xhk118" - }, - { - "label": "San Antonio Military Medical Center", - "type": "Related", - "id": "https://ror.org/00m1mwc36" - }, - { - "label": "The University of Texas System", - "type": "Parent", - "id": "https://ror.org/01gek1696" - } - ], - "addresses": [ - { - "lat": 29.508129, - "lng": -98.574025, - "state": "Texas", - "state_code": "US-TX", - "city": "San Antonio", - "geonames_city": { - "id": 4726206, - "city": "San Antonio", - "geonames_admin1": { - "name": "Texas", - "id": 4736286, - "ascii_name": "Texas", - "code": "US.TX" - }, - "geonames_admin2": { - "name": "Bexar County", - "id": 4674023, - "ascii_name": "Bexar County", - "code": "US.TX.029" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } - }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" } - ], - "links": [ - "http://www.uthscsa.edu/" - ], - "aliases": [ - - ], - "acronyms": [ - "UTHSCSA" - ], - "status": "active", - "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Texas_Health_Science_Center_at_San_Antonio", - "labels": [ - - ], - "country": { - "country_name": "United States", - "country_code": "US" }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "uacs.edu.mk" + ], + "established": 2005, + "external_ids": [ + { "all": [ - "0000 0001 0629 5880" - ] + "grid.445944.c" + ], + "preferred": "grid.445944.c", + "type": "grid" }, - "FundRef": { - "preferred": "100008635", + { "all": [ - "100008635", - "100008636" - ] + "0000 0004 0446 4427" + ], + "preferred": "0000 0004 0446 4427", + "type": "isni" }, - "OrgRef": { - "preferred": null, + { "all": [ - "1593427" - ] - }, - "Wikidata": { + "Q7894510" + ], "preferred": null, - "all": [ - "Q4005868" - ] - }, - "GRID": { - "preferred": "grid.267309.9", - "all": "grid.267309.9" + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/01sps7q28", - "name": "The University of Texas Health Science Center at Tyler", - "email_address": null, - "ip_addresses": [ - ], - "established": 1947, - "types": [ - "Healthcare" + "id": "https://ror.org/05hknds03", + "links": [ + { + "type": "website", + "value": "https://uacs.edu.mk" + }, + { + "type": "wikipedia", + "value": "https://en.wikipedia.org/wiki/University_American_College_Skopje" + } ], - "relationships": [ + "locations": [ { - "label": "The University of Texas System", - "type": "Parent", - "id": "https://ror.org/01gek1696" - } - ], - "addresses": [ - { - "lat": 32.426014, - "lng": -95.212728, - "state": "Texas", - "state_code": "US-TX", - "city": "Tyler", - "geonames_city": { - "id": 4738214, - "city": "Tyler", - "geonames_admin1": { - "name": "Texas", - "id": 4736286, - "ascii_name": "Texas", - "code": "US.TX" - }, - "geonames_admin2": { - "name": "Smith County", - "id": 4729130, - "ascii_name": "Smith County", - "code": "US.TX.423" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "EU", + "continent_name": "Europe", + "country_code": "MK", + "country_name": "North Macedonia", + "country_subdivision_code": null, + "country_subdivision_name": "Grad Skopje", + "lat": 41.99646, + "lng": 21.43141, + "name": "Skopje" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 785842 } ], - "links": [ - "https://www.utsystem.edu/institutions/university-texas-health-science-center-tyler" - ], - "aliases": [ - "East Texas Tuberculosis Sanitarium", - "UT Health Northeast" - ], - "acronyms": [ - "UTHSCT" - ], - "status": "active", - "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Texas_Health_Science_Center_at_Tyler", - "labels": [ - - ], - "country": { - "country_name": "United States", - "country_code": "US" - }, - "external_ids": { - "ISNI": { - "preferred": null, - "all": [ - "0000 0000 9704 5790" - ] - }, - "OrgRef": { - "preferred": null, - "all": [ - "3446655" - ] - }, - "Wikidata": { - "preferred": null, - "all": [ - "Q7896437" - ] + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "UACS" }, - "GRID": { - "preferred": "grid.267310.1", - "all": "grid.267310.1" + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "University American College Skopje" } - } - }, - { - "id": "https://ror.org/05byvp690", - "name": "The University of Texas Southwestern Medical Center", - "email_address": null, - "ip_addresses": [ - ], - "established": 1943, + "relationships": [], + "status": "active", "types": [ - "Healthcare" + "education" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } + }, + "domains": [ + "3-5lab.fr" ], - "relationships": [ + "established": 2004, + "external_ids": [ { - "label": "Children's Medical Center", - "type": "Related", - "id": "https://ror.org/02ndk3y82" - }, - { - "label": "Parkland Memorial Hospital", - "type": "Related", - "id": "https://ror.org/0208r0146" - }, - { - "label": "VA North Texas Health Care System", - "type": "Related", - "id": "https://ror.org/01nzxq896" - }, - { - "label": "The University of Texas System", - "type": "Parent", - "id": "https://ror.org/01gek1696" - }, - { - "label": "Institute for Exercise and Environmental Medicine", - "type": "Child", - "id": "https://ror.org/03gqc7y13" - }, - { - "label": "Texas Health Dallas", - "type": "Child", - "id": "https://ror.org/05k07p323" - } - ], - "addresses": [ - { - "lat": 32.812185, - "lng": -96.840174, - "state": "Texas", - "state_code": "US-TX", - "city": "Dallas", - "geonames_city": { - "id": 4684888, - "city": "Dallas", - "geonames_admin1": { - "name": "Texas", - "id": 4736286, - "ascii_name": "Texas", - "code": "US.TX" - }, - "geonames_admin2": { - "name": "Dallas County", - "id": 4684904, - "ascii_name": "Dallas County", - "code": "US.TX.113" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } - }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "all": [ + "grid.424877.a" + ], + "preferred": "grid.424877.a", + "type": "grid" } ], + "id": "https://ror.org/0509ggw88", "links": [ - "http://www.utsouthwestern.edu/" + { + "type": "website", + "value": "https://www.3-5lab.fr" + } ], - "aliases": [ - "UT Southwestern" + "locations": [ + { + "geonames_details": { + "continent_code": "EU", + "continent_name": "Europe", + "country_code": "FR", + "country_name": "France", + "country_subdivision_code": "IDF", + "country_subdivision_name": "Île-de-France", + "lat": 48.64026, + "lng": 2.23858, + "name": "Marcoussis" + }, + "geonames_id": 2995916 + } ], - "acronyms": [ - + "names": [ + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "III V Lab" + } ], + "relationships": [], "status": "active", - "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Texas_Southwestern_Medical_Center", - "labels": [ - - ], - "country": { - "country_name": "United States", - "country_code": "US" + "types": [ + "facility" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "dnb.nl" + ], + "established": 1814, + "external_ids": [ + { "all": [ - "0000 0000 9482 7121" - ] + "501100014104" + ], + "preferred": "501100014104", + "type": "fundref" }, - "FundRef": { - "preferred": "100007914", + { "all": [ - "100007914", - "100010487", - "100008260" - ] + "grid.459463.9" + ], + "preferred": "grid.459463.9", + "type": "grid" }, - "OrgRef": { - "preferred": null, + { "all": [ - "617906" - ] - }, - "Wikidata": { + "0000 0004 0369 4300" + ], "preferred": null, - "all": [ - "Q2725999" - ] + "type": "isni" }, - "GRID": { - "preferred": "grid.267313.2", - "all": "grid.267313.2" + { + "all": [ + "Q1180205" + ], + "preferred": null, + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/019kgqr73", - "name": "The University of Texas at Arlington", - "email_address": "", - "ip_addresses": [ - ], - "established": 1895, - "types": [ - "Education" + "id": "https://ror.org/02fabx761", + "links": [ + { + "type": "website", + "value": "https://www.dnb.nl" + }, + { + "type": "wikipedia", + "value": "https://en.wikipedia.org/wiki/De_Nederlandsche_Bank" + } ], - "relationships": [ + "locations": [ { - "label": "VA North Texas Health Care System", - "type": "Related", - "id": "https://ror.org/01nzxq896" - }, - { - "label": "The University of Texas System", - "type": "Parent", - "id": "https://ror.org/01gek1696" - } - ], - "addresses": [ - { - "lat": 32.731, - "lng": -97.115, - "state": "Texas", - "state_code": "US-TX", - "city": "Arlington", - "geonames_city": { - "id": 4671240, - "city": "Arlington", - "geonames_admin1": { - "name": "Texas", - "id": 4736286, - "ascii_name": "Texas", - "code": "US.TX" - }, - "geonames_admin2": { - "name": "Tarrant County", - "id": 4735638, - "ascii_name": "Tarrant County", - "code": "US.TX.439" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "EU", + "continent_name": "Europe", + "country_code": "NL", + "country_name": "The Netherlands", + "country_subdivision_code": "NH", + "country_subdivision_name": "North Holland", + "lat": 52.37403, + "lng": 4.88969, + "name": "Amsterdam" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 2759794 } ], - "links": [ - "http://www.uta.edu/uta/" - ], - "aliases": [ - "UT Arlington" - ], - "acronyms": [ - "UTA" - ], - "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Texas_at_Arlington", - "labels": [ + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "DNB" + }, + { + "lang": "nl", + "types": [ + "ror_display", + "label" + ], + "value": "De Nederlandsche Bank" + }, { - "label": "Université du Texas à Arlington", - "iso639": "fr" + "lang": "en", + "types": [ + "alias" + ], + "value": "Dutch Bank" } ], - "country": { - "country_name": "United States", - "country_code": "US" + "relationships": [], + "status": "active", + "types": [ + "funder", + "other" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "kfupm.edu.sa" + ], + "established": 1963, + "external_ids": [ + { "all": [ - "0000 0001 2181 9515" - ] - }, - "FundRef": { + "501100004055" + ], "preferred": null, - "all": [ - "100009497" - ] + "type": "fundref" }, - "OrgRef": { - "preferred": null, + { "all": [ - "906409" - ] + "grid.412135.0" + ], + "preferred": "grid.412135.0", + "type": "grid" }, - "Wikidata": { - "preferred": null, + { "all": [ - "Q1230739" - ] + "0000 0001 1091 0356" + ], + "preferred": null, + "type": "isni" }, - "GRID": { - "preferred": "grid.267315.4", - "all": "grid.267315.4" + { + "all": [ + "Q4116241" + ], + "preferred": null, + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/051smbs96", - "name": "The University of Texas of the Permian Basin", - "email_address": null, - "ip_addresses": [ - ], - "established": 1973, - "types": [ - "Education" + "id": "https://ror.org/03yez3163", + "links": [ + { + "type": "website", + "value": "https://www.kfupm.edu.sa" + }, + { + "type": "wikipedia", + "value": "http://en.wikipedia.org/wiki/King_Fahd_University_of_Petroleum_and_Minerals" + } ], - "relationships": [ + "locations": [ { - "label": "The University of Texas System", - "type": "Parent", - "id": "https://ror.org/01gek1696" - } - ], - "addresses": [ - { - "lat": 31.889444, - "lng": -102.329531, - "state": "Texas", - "state_code": "US-TX", - "city": "Odessa", - "geonames_city": { - "id": 5527554, - "city": "Odessa", - "geonames_admin1": { - "name": "Texas", - "id": 4736286, - "ascii_name": "Texas", - "code": "US.TX" - }, - "geonames_admin2": { - "name": "Ector County", - "id": 5520910, - "ascii_name": "Ector County", - "code": "US.TX.135" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "AS", + "continent_name": "Asia", + "country_code": "SA", + "country_name": "Saudi Arabia", + "country_subdivision_code": "04", + "country_subdivision_name": "Eastern Province", + "lat": 26.28864, + "lng": 50.11396, + "name": "Dhahran" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 107797 } ], - "links": [ - "http://www.utpb.edu/" - ], - "aliases": [ - "UT Permian Basin" - ], - "acronyms": [ - "UTPB" + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "KFUPM" + }, + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "King Fahd University of Petroleum and Minerals" + }, + { + "lang": "ar", + "types": [ + "label" + ], + "value": "جامعة الملك فهد للبترول والمعادن" + } ], + "relationships": [], "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Texas_of_the_Permian_Basin", - "labels": [ - - ], - "country": { - "country_name": "United States", - "country_code": "US" + "types": [ + "education", + "funder" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [], + "established": null, + "external_ids": [ + { "all": [ - "0000 0000 9140 1491" - ] - }, - "OrgRef": { + "100006445" + ], "preferred": null, - "all": [ - "1419441" - ] + "type": "fundref" }, - "Wikidata": { - "preferred": null, + { "all": [ - "Q2495935" - ] - }, - "GRID": { - "preferred": "grid.267328.a", - "all": "grid.267328.a" + "grid.457570.4" + ], + "preferred": "grid.457570.4", + "type": "grid" } - } - }, - { - "id": "https://ror.org/044vy1d05", - "name": "Tokushima University", - "email_address": "", - "ip_addresses": [ - ], - "established": 1949, - "types": [ - "Education" + "id": "https://ror.org/043trmd87", + "links": [ + { + "type": "website", + "value": "http://chm.pse.umass.edu/" + } ], - "relationships": [ + "locations": [ { - "label": "Tokushima University Hospital", - "type": "Related", - "id": "https://ror.org/021ph5e41" - } - ], - "addresses": [ - { - "lat": 34.07, - "lng": 134.56, - "state": null, - "state_code": null, - "city": "Tokushima", - "geonames_city": { - "id": 1850158, - "city": "Tokushima", - "geonames_admin1": { - "name": "Tokushima", - "id": 1850157, - "ascii_name": "Tokushima", - "code": "JP.39" - }, - "geonames_admin2": { - "name": "Tokushima Shi", - "id": 1850156, - "ascii_name": "Tokushima Shi", - "code": "JP.39.1850156" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "NA", + "continent_name": "North America", + "country_code": "US", + "country_name": "United States", + "country_subdivision_code": "MA", + "country_subdivision_name": "Massachusetts", + "lat": 42.37537, + "lng": -72.51925, + "name": "Amherst Center" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 1861060 + "geonames_id": 4929023 } ], - "links": [ - "https://www.tokushima-u.ac.jp/" - ], - "aliases": [ - "Tokushima Daigaku", - "University of Tokushima" - ], - "acronyms": [ - + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "CHM" + }, + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "Center for Hierarchical Manufacturing" + } ], - "status": "active", - "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Tokushima", - "labels": [ + "relationships": [ + { + "label": "U.S. National Science Foundation", + "type": "related", + "id": "https://ror.org/021nxhr62" + }, { - "label": "徳島大学", - "iso639": "ja" + "label": "University of Massachusetts Amherst", + "type": "related", + "id": "https://ror.org/0072zz521" } ], - "country": { - "country_name": "Japan", - "country_code": "JP" + "status": "active", + "types": [ + "funder", + "nonprofit" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "musashino-u.ac.jp" + ], + "established": 1924, + "external_ids": [ + { "all": [ - "0000 0001 1092 3579" - ] + "100019640" + ], + "preferred": "100019640", + "type": "fundref" }, - "FundRef": { - "preferred": null, + { "all": [ - "501100005623" - ] + "grid.411867.d" + ], + "preferred": "grid.411867.d", + "type": "grid" }, - "OrgRef": { - "preferred": null, + { "all": [ - "15696836" - ] - }, - "Wikidata": { + "0000 0001 0356 8417" + ], "preferred": null, - "all": [ - "Q1150231" - ] + "type": "isni" }, - "GRID": { - "preferred": "grid.267335.6", - "all": "grid.267335.6" + { + "all": [ + "Q6940182" + ], + "preferred": null, + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/03np13864", - "name": "University of Trinidad and Tobago", - "email_address": null, - "ip_addresses": [ - ], - "established": 2004, - "types": [ - "Education" + "id": "https://ror.org/04bcbax71", + "links": [ + { + "type": "website", + "value": "https://www.musashino-u.ac.jp" + }, + { + "type": "wikipedia", + "value": "http://en.wikipedia.org/wiki/Musashino_University" + } ], - "relationships": [ - - ], - "addresses": [ - { - "lat": 10.616667, - "lng": -61.216667, - "state": null, - "state_code": null, - "city": "Arima", - "geonames_city": { - "id": 3575051, - "city": "Arima", - "geonames_admin1": { - "name": "Borough of Arima", - "id": 3575052, - "ascii_name": "Borough of Arima", - "code": "TT.01" - }, - "geonames_admin2": { - "name": null, - "id": null, - "ascii_name": null, - "code": null - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "locations": [ + { + "geonames_details": { + "continent_code": "AS", + "continent_name": "Asia", + "country_code": "JP", + "country_name": "Japan", + "country_subdivision_code": "13", + "country_subdivision_name": "Tokyo", + "lat": 35.6895, + "lng": 139.69171, + "name": "Tokyo" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 3573591 + "geonames_id": 1850147 } ], - "links": [ - "https://utt.edu.tt/" - ], - "aliases": [ - - ], - "acronyms": [ - "UTT" - ], - "status": "active", - "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Trinidad_and_Tobago", - "labels": [ + "names": [ + { + "lang": null, + "types": [ + "alias" + ], + "value": "Musashino Daigaku" + }, + { + "lang": null, + "types": [ + "ror_display", + "label" + ], + "value": "Musashino University" + }, { - "label": "Universidad de Trinidad y Tobago", - "iso639": "es" + "lang": "ja", + "types": [ + "label" + ], + "value": "武蔵野大学" } ], - "country": { - "country_name": "Trinidad and Tobago", - "country_code": "TT" + "relationships": [], + "status": "active", + "types": [ + "education", + "funder" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "hansung.ac.kr" + ], + "established": 1972, + "external_ids": [ + { "all": [ - "0000 0000 9490 0886" - ] - }, - "OrgRef": { + "501100002491" + ], "preferred": null, + "type": "fundref" + }, + { "all": [ - "8706288" - ] + "grid.444079.a" + ], + "preferred": "grid.444079.a", + "type": "grid" }, - "Wikidata": { - "preferred": null, + { "all": [ - "Q648244" - ] + "0000 0004 0532 678X" + ], + "preferred": null, + "type": "isni" }, - "GRID": { - "preferred": "grid.267355.0", - "all": "grid.267355.0" + { + "all": [ + "Q482765" + ], + "preferred": null, + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/04wn28048", - "name": "University of Tulsa", - "email_address": "", - "ip_addresses": [ - ], - "established": 1894, - "types": [ - "Education" + "id": "https://ror.org/048m9x696", + "links": [ + { + "type": "website", + "value": "https://www.hansung.ac.kr" + }, + { + "type": "wikipedia", + "value": "https://en.wikipedia.org/wiki/Hansung_University" + } ], - "relationships": [ - - ], - "addresses": [ - { - "lat": 36.152222, - "lng": -95.946389, - "state": "Oklahoma", - "state_code": "US-OK", - "city": "Tulsa", - "geonames_city": { - "id": 4553433, - "city": "Tulsa", - "geonames_admin1": { - "name": "Oklahoma", - "id": 4544379, - "ascii_name": "Oklahoma", - "code": "US.OK" - }, - "geonames_admin2": { - "name": "Tulsa County", - "id": 4553440, - "ascii_name": "Tulsa County", - "code": "US.OK.143" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "locations": [ + { + "geonames_details": { + "continent_code": "AS", + "continent_name": "Asia", + "country_code": "KR", + "country_name": "South Korea", + "country_subdivision_code": "11", + "country_subdivision_name": "Seoul", + "lat": 37.566, + "lng": 126.9784, + "name": "Seoul" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 1835848 } ], - "links": [ - "http://utulsa.edu/" - ], - "aliases": [ - - ], - "acronyms": [ - "TU" - ], - "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Tulsa", - "labels": [ + "names": [ + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "Hansung University" + }, + { + "lang": "en", + "types": [ + "alias" + ], + "value": "Hansung Woman's University" + }, { - "label": "Université de tulsa", - "iso639": "fr" + "lang": "ko", + "types": [ + "label" + ], + "value": "한성대학교" } ], - "country": { - "country_name": "United States", - "country_code": "US" + "relationships": [], + "status": "active", + "types": [ + "education", + "funder" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "tuh.ie" + ], + "established": 1996, + "external_ids": [ + { "all": [ - "0000 0001 2160 264X" - ] + "grid.413305.0" + ], + "preferred": "grid.413305.0", + "type": "grid" }, - "FundRef": { - "preferred": "100007147", + { "all": [ - "100007147", - "100006455" - ] - }, - "OrgRef": { + "0000 0004 0617 5936" + ], "preferred": null, - "all": [ - "32043" - ] + "type": "isni" }, - "Wikidata": { - "preferred": null, + { "all": [ - "Q1848657" - ] - }, - "GRID": { - "preferred": "grid.267360.6", - "all": "grid.267360.6" + "Q7680014" + ], + "preferred": null, + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/04scfb908", - "name": "Alfred Health", - "email_address": null, - "ip_addresses": [ - ], - "established": 1871, - "types": [ - "Healthcare" + "id": "https://ror.org/01fvmtt37", + "links": [ + { + "type": "website", + "value": "https://www.tuh.ie" + }, + { + "type": "wikipedia", + "value": "https://en.wikipedia.org/wiki/Tallaght_Hospital" + } ], - "relationships": [ + "locations": [ { - "label": "Caulfield Hospital", - "type": "Child", - "id": "https://ror.org/01fcxf261" - }, - { - "label": "Melbourne Sexual Health Centre", - "type": "Child", - "id": "https://ror.org/013fdz725" - }, - { - "label": "National Trauma Research Institute", - "type": "Child", - "id": "https://ror.org/048t93218" - }, - { - "label": "The Alfred Hospital", - "type": "Child", - "id": "https://ror.org/01wddqe20" - } - ], - "addresses": [ - { - "lat": -37.845542, - "lng": 144.981632, - "state": "Victoria", - "state_code": "AU-VIC", - "city": "Melbourne", - "geonames_city": { - "id": 2158177, - "city": "Melbourne", - "geonames_admin1": { - "name": "Victoria", - "id": 2145234, - "ascii_name": "Victoria", - "code": "AU.07" - }, - "geonames_admin2": { - "name": "Melbourne", - "id": 7839805, - "ascii_name": "Melbourne", - "code": "AU.07.24600" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "EU", + "continent_name": "Europe", + "country_code": "IE", + "country_name": "Ireland", + "country_subdivision_code": "L", + "country_subdivision_name": "Leinster", + "lat": 53.33306, + "lng": -6.24889, + "name": "Dublin" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 2077456 + "geonames_id": 2964574 } ], - "links": [ - "http://www.alfred.org.au/" - ], - "aliases": [ - - ], - "acronyms": [ - - ], - "status": "active", - "wikipedia_url": "", - "labels": [ - - ], - "country": { - "country_name": "Australia", - "country_code": "AU" - }, - "external_ids": { - "ISNI": { - "preferred": null, - "all": [ - "0000 0004 0432 5259" - ] + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "AMNCH" }, - "FundRef": { - "preferred": null, - "all": [ - "501100002716" - ] + { + "lang": "en", + "types": [ + "alias" + ], + "value": "Adelaide and Meath Hospital, Dublin, incorporating the National Children's Hospital" + }, + { + "lang": "ga", + "types": [ + "alias" + ], + "value": "Ospidéal Adelaide agus na Mí, Baile Átha Cliath, ina gcorpraítear Ospidéal Náisiúnta na Leanaí" }, - "GRID": { - "preferred": "grid.267362.4", - "all": "grid.267362.4" + { + "lang": "ga", + "types": [ + "label" + ], + "value": "Ospidéal Ollscoile Thamhlachta" + }, + { + "lang": "ga", + "types": [ + "alias" + ], + "value": "Ospidéal Thamhlachta" + }, + { + "lang": "en", + "types": [ + "alias" + ], + "value": "Tallaght Hospital" + }, + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "Tallaght University Hospital" } - } - }, - { - "id": "https://ror.org/02c2f8975", - "name": "University of Ulsan", - "email_address": null, - "ip_addresses": [ - - ], - "established": 1970, - "types": [ - "Education" ], "relationships": [ { - "label": "Ulsan University Hospital", - "type": "Related", - "id": "https://ror.org/03sab2a45" - } - ], - "addresses": [ - { - "lat": 35.542772, - "lng": 129.256725, - "state": null, - "state_code": null, - "city": "Ulsan", - "geonames_city": { - "id": 1833747, - "city": "Ulsan", - "geonames_admin1": { - "name": "Ulsan", - "id": 1833742, - "ascii_name": "Ulsan", - "code": "KR.21" - }, - "geonames_admin2": { - "name": null, - "id": null, - "ascii_name": null, - "code": null - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } - }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 1835841 + "label": "Trinity College Dublin", + "type": "related", + "id": "https://ror.org/02tyrky19" } ], - "links": [ - "http://en.ulsan.ac.kr/contents/main/" - ], - "aliases": [ - - ], - "acronyms": [ - "UOU" - ], "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Ulsan", - "labels": [ - { - "label": "울산대학교", - "iso639": "ko" + "types": [ + "healthcare" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" } - ], - "country": { - "country_name": "South Korea", - "country_code": "KR" }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "mseuf.edu.ph" + ], + "established": 1947, + "external_ids": [ + { "all": [ - "0000 0004 0533 4667" - ] + "grid.448687.1" + ], + "preferred": "grid.448687.1", + "type": "grid" }, - "FundRef": { - "preferred": null, + { "all": [ - "501100002568" - ] + "0000 0004 0639 6528" + ], + "preferred": null, + "type": "isni" }, - "OrgRef": { - "preferred": "10458246", + { "all": [ - "10458246", - "15162872" - ] - }, - "Wikidata": { + "Q3578221" + ], "preferred": null, - "all": [ - "Q491717" - ] - }, - "GRID": { - "preferred": "grid.267370.7", - "all": "grid.267370.7" + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/010acrp16", - "name": "University of West Alabama", - "email_address": null, - "ip_addresses": [ - ], - "established": 1835, - "types": [ - "Education" + "id": "https://ror.org/02fhfq388", + "links": [ + { + "type": "website", + "value": "https://mseuf.edu.ph" + }, + { + "type": "wikipedia", + "value": "https://en.wikipedia.org/wiki/Enverga_University" + } ], - "relationships": [ - - ], - "addresses": [ - { - "lat": 32.59, - "lng": -88.186, - "state": "Alabama", - "state_code": "US-AL", - "city": "Livingston", - "geonames_city": { - "id": 4073383, - "city": "Livingston", - "geonames_admin1": { - "name": "Alabama", - "id": 4829764, - "ascii_name": "Alabama", - "code": "US.AL" - }, - "geonames_admin2": { - "name": "Sumter County", - "id": 4092386, - "ascii_name": "Sumter County", - "code": "US.AL.119" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "locations": [ + { + "geonames_details": { + "continent_code": "AS", + "continent_name": "Asia", + "country_code": "PH", + "country_name": "Philippines", + "country_subdivision_code": "40", + "country_subdivision_name": "Calabarzon", + "lat": 13.93139, + "lng": 121.61722, + "name": "Lucena City" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 1705357 } ], - "links": [ - "http://www.uwa.edu/" - ], - "aliases": [ - "Livingston Female Academy" - ], - "acronyms": [ - "UWA" + "names": [ + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "Enverga University" + }, + { + "lang": null, + "types": [ + "acronym" + ], + "value": "MSEUF" + }, + { + "lang": "en", + "types": [ + "alias" + ], + "value": "Manuel S. Enverga University Foundation" + } ], + "relationships": [], "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_West_Alabama", - "labels": [ - - ], - "country": { - "country_name": "United States", - "country_code": "US" + "types": [ + "education" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "plus.ac.at" + ], + "established": 1622, + "external_ids": [ + { "all": [ - "0000 0000 9963 9197" - ] - }, - "OrgRef": { + "501100005644" + ], "preferred": null, + "type": "fundref" + }, + { "all": [ - "2425212" - ] + "grid.7039.d" + ], + "preferred": "grid.7039.d", + "type": "grid" }, - "Wikidata": { - "preferred": null, + { "all": [ - "Q637346" - ] + "0000 0001 1015 6330" + ], + "preferred": null, + "type": "isni" }, - "GRID": { - "preferred": "grid.267434.0", - "all": "grid.267434.0" + { + "all": [ + "Q27265" + ], + "preferred": null, + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/002w4zy91", - "name": "University of West Florida", - "email_address": null, - "ip_addresses": [ - ], - "established": 1963, - "types": [ - "Education" + "id": "https://ror.org/05gs8cd61", + "links": [ + { + "type": "website", + "value": "https://www.plus.ac.at/" + }, + { + "type": "wikipedia", + "value": "http://en.wikipedia.org/wiki/University_of_Salzburg" + } ], - "relationships": [ + "locations": [ { - "label": "State University System of Florida", - "type": "Parent", - "id": "https://ror.org/05sqd3t97" - } - ], - "addresses": [ - { - "lat": 30.549493, - "lng": -87.21812, - "state": "Florida", - "state_code": "US-FL", - "city": "Pensacola", - "geonames_city": { - "id": 4168228, - "city": "Pensacola", - "geonames_admin1": { - "name": "Florida", - "id": 4155751, - "ascii_name": "Florida", - "code": "US.FL" - }, - "geonames_admin2": { - "name": "Escambia County", - "id": 4154550, - "ascii_name": "Escambia County", - "code": "US.FL.033" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "EU", + "continent_name": "Europe", + "country_code": "AT", + "country_name": "Austria", + "country_subdivision_code": "5", + "country_subdivision_name": "Salzburg", + "lat": 47.79941, + "lng": 13.04399, + "name": "Salzburg" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 2766824 } ], - "links": [ - "http://uwf.edu/" - ], - "aliases": [ - - ], - "acronyms": [ - "UWF" - ], - "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_West_Florida", - "labels": [ - - ], - "country": { - "country_name": "United States", - "country_code": "US" - }, - "external_ids": { - "ISNI": { - "preferred": null, - "all": [ - "0000 0001 2112 2427" - ] - }, - "FundRef": { - "preferred": null, - "all": [ - "100009842" - ] + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "PLUS" }, - "OrgRef": { - "preferred": null, - "all": [ - "750756" - ] + { + "lang": "en", + "types": [ + "alias" + ], + "value": "Paris Lodron University of Salzburg" }, - "Wikidata": { - "preferred": null, - "all": [ - "Q659255" - ] + { + "lang": "de", + "types": [ + "label" + ], + "value": "Paris-Lodron-Universität Salzburg" }, - "GRID": { - "preferred": "grid.267436.2", - "all": "grid.267436.2" + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "University of Salzburg" } - } + ], + "relationships": [], + "status": "active", + "types": [ + "education", + "funder" + ] }, { - "id": "https://ror.org/01cqxk816", - "name": "University of West Georgia", - "email_address": null, - "ip_addresses": [ - + "admin": { + "created": { + "date": "2024-11-18", + "schema_version": "2.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } + }, + "domains": [ + "hch.tums.ac.ir" ], - "established": 1906, - "types": [ - "Education" + "established": null, + "external_ids": [], + "id": "https://ror.org/04hgqjy83", + "links": [ + { + "type": "website", + "value": "https://hch.tums.ac.ir" + } ], - "relationships": [ + "locations": [ { - "label": "University System of Georgia", - "type": "Parent", - "id": "https://ror.org/017wcm924" - } - ], - "addresses": [ - { - "lat": 33.573357, - "lng": -85.099593, - "state": "Georgia", - "state_code": "US-GA", - "city": "Carrollton", - "geonames_city": { - "id": 4186416, - "city": "Carrollton", - "geonames_admin1": { - "name": "Georgia", - "id": 4197000, - "ascii_name": "Georgia", - "code": "US.GA" - }, - "geonames_admin2": { - "name": "Carroll County", - "id": 4186396, - "ascii_name": "Carroll County", - "code": "US.GA.045" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "AS", + "continent_name": "Asia", + "country_code": "IR", + "country_name": "Iran", + "country_subdivision_code": "23", + "country_subdivision_name": "Tehran", + "lat": 35.69439, + "lng": 51.42151, + "name": "Tehran" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 112931 } ], - "links": [ - "http://www.westga.edu/" - ], - "aliases": [ - + "names": [ + { + "lang": "en", + "types": [ + "label", + "ror_display" + ], + "value": "Hakim Children Hospital" + }, + { + "lang": "en", + "types": [ + "alias" + ], + "value": "Hakim Children's Hospital" + }, + { + "lang": "fa", + "types": [ + "label" + ], + "value": "بیمارستان کودکان حکیم" + }, + { + "lang": "fa", + "types": [ + "alias" + ], + "value": "بیمارستان کودکان حکیم دانشگاه علوم پزشکی تهران" + } ], - "acronyms": [ - "UWG" + "relationships": [ + { + "label": "Tehran University of Medical Sciences", + "type": "related", + "id": "https://ror.org/01c4pz451" + } ], "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_West_Georgia", - "labels": [ - - ], - "country": { - "country_name": "United States", - "country_code": "US" + "types": [ + "healthcare" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "pstu.ac.bd" + ], + "established": 2000, + "external_ids": [ + { "all": [ - "0000 0001 2223 6696" - ] + "501100014587" + ], + "preferred": "501100014587", + "type": "fundref" }, - "FundRef": { - "preferred": null, + { "all": [ - "100007922" - ] + "grid.443081.a" + ], + "preferred": "grid.443081.a", + "type": "grid" }, - "OrgRef": { - "preferred": null, + { "all": [ - "595315" - ] - }, - "Wikidata": { + "0000 0004 0489 3643" + ], "preferred": null, - "all": [ - "Q2495945" - ] + "type": "isni" }, - "GRID": { - "preferred": "grid.267437.3", - "all": "grid.267437.3" + { + "all": [ + "Q7148748" + ], + "preferred": null, + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/03c8vvr84", - "name": "University of Western States", - "email_address": null, - "ip_addresses": [ - ], - "established": 1904, - "types": [ - "Education" + "id": "https://ror.org/03m50n726", + "links": [ + { + "type": "website", + "value": "https://www.pstu.ac.bd" + }, + { + "type": "wikipedia", + "value": "https://en.wikipedia.org/wiki/Patuakhali_Science_and_Technology_University" + } ], - "relationships": [ - - ], - "addresses": [ - { - "lat": 45.543351, - "lng": -122.523973, - "state": "Oregon", - "state_code": "US-OR", - "city": "Portland", - "geonames_city": { - "id": 5746545, - "city": "Portland", - "geonames_admin1": { - "name": "Oregon", - "id": 5744337, - "ascii_name": "Oregon", - "code": "US.OR" - }, - "geonames_admin2": { - "name": "Multnomah County", - "id": 5742126, - "ascii_name": "Multnomah County", - "code": "US.OR.051" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "locations": [ + { + "geonames_details": { + "continent_code": "AS", + "continent_name": "Asia", + "country_code": "BD", + "country_name": "Bangladesh", + "country_subdivision_code": "A", + "country_subdivision_name": "Barisal Division", + "lat": 22.33333, + "lng": 90.33333, + "name": "Patuakhali" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 1337216 } ], - "links": [ - "http://www.uws.edu/" - ], - "aliases": [ - "Western States Chiropractic College" - ], - "acronyms": [ - "UWS" + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "PSTU" + }, + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "Patuakhali Science and Technology University" + }, + { + "lang": "bn", + "types": [ + "label" + ], + "value": "পটুয়াখালী বিজ্ঞান ও প্রযুক্তি বিশ্ববিদ্যালয়" + } ], + "relationships": [], "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Western_States", - "labels": [ - - ], - "country": { - "country_name": "United States", - "country_code": "US" + "types": [ + "education", + "funder" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "aih-net.com" + ], + "established": 1918, + "external_ids": [ + { "all": [ - "0000 0004 0455 9493" - ] + "grid.413984.3" + ], + "preferred": "grid.413984.3", + "type": "grid" }, - "OrgRef": { - "preferred": null, + { "all": [ - "1655050" - ] - }, - "Wikidata": { + "Q11666229" + ], "preferred": null, - "all": [ - "Q7896612" - ] - }, - "GRID": { - "preferred": "grid.267451.3", - "all": "grid.267451.3" + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/03fmjzx88", - "name": "University of Winchester", - "email_address": null, - "ip_addresses": [ - ], - "established": 1840, - "types": [ - "Education" + "id": "https://ror.org/04tg98e93", + "links": [ + { + "type": "website", + "value": "https://aih-net.com" + } ], - "relationships": [ - - ], - "addresses": [ - { - "lat": 51.060338, - "lng": -1.325418, - "state": null, - "state_code": null, - "city": "Winchester", - "geonames_city": { - "id": 2633858, - "city": "Winchester", - "geonames_admin1": { - "name": "England", - "id": 6269131, - "ascii_name": "England", - "code": "GB.ENG" - }, - "geonames_admin2": { - "name": "Hampshire", - "id": 2647554, - "ascii_name": "Hampshire", - "code": "GB.ENG.F2" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": "SOUTH EAST (ENGLAND)", - "code": "UKJ" - }, - "nuts_level2": { - "name": "Hampshire and Isle of Wight", - "code": "UKJ3" - }, - "nuts_level3": { - "name": "Central Hampshire", - "code": "UKJ36" - } + "locations": [ + { + "geonames_details": { + "continent_code": "AS", + "continent_name": "Asia", + "country_code": "JP", + "country_name": "Japan", + "country_subdivision_code": "40", + "country_subdivision_name": "Fukuoka", + "lat": 33.63654, + "lng": 130.68678, + "name": "Iizuka" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 2635167 + "geonames_id": 1861835 } ], - "links": [ - "http://www.winchester.ac.uk/pages/home.aspx" - ], - "aliases": [ - - ], - "acronyms": [ - + "names": [ + { + "lang": null, + "types": [ + "ror_display", + "label" + ], + "value": "Aso Iizuka Hospital" + }, + { + "lang": "ja", + "types": [ + "label" + ], + "value": "飯塚病院" + } ], + "relationships": [], "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Winchester", - "labels": [ - - ], - "country": { - "country_name": "United Kingdom", - "country_code": "GB" - }, - "external_ids": { - "ISNI": { - "preferred": null, - "all": [ - "0000 0000 9422 2878" - ] + "types": [ + "healthcare" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" }, - "FundRef": { - "preferred": null, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } + }, + "domains": [ + "imsciences.edu.pk" + ], + "established": 1995, + "external_ids": [ + { "all": [ - "100010057" - ] + "grid.444989.c" + ], + "preferred": "grid.444989.c", + "type": "grid" }, - "HESA": { - "preferred": null, + { "all": [ - "0021" - ] - }, - "UCAS": { + "0000 0004 0609 2495" + ], "preferred": null, - "all": [ - "W76" - ] + "type": "isni" }, - "UKPRN": { - "preferred": null, + { "all": [ - "10003614" - ] - }, - "OrgRef": { + "Q15983147" + ], "preferred": null, - "all": [ - "3140939" - ] + "type": "wikidata" + } + ], + "id": "https://ror.org/02m8e1r74", + "links": [ + { + "type": "website", + "value": "https://imsciences.edu.pk" }, - "Wikidata": { - "preferred": null, - "all": [ - "Q3551690" - ] + { + "type": "wikipedia", + "value": "https://en.wikipedia.org/wiki/Institute_of_Management_Sciences_(Peshawar)" + } + ], + "locations": [ + { + "geonames_details": { + "continent_code": "AS", + "continent_name": "Asia", + "country_code": "PK", + "country_name": "Pakistan", + "country_subdivision_code": "KP", + "country_subdivision_name": "Khyber Pakhtunkhwa", + "lat": 34.008, + "lng": 71.57849, + "name": "Peshawar" + }, + "geonames_id": 1168197 + } + ], + "names": [ + { + "lang": null, + "types": [ + "alias" + ], + "value": "IMSciences" }, - "GRID": { - "preferred": "grid.267454.6", - "all": "grid.267454.6" + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "Institute of Management Sciences Peshawar" } - } - }, - { - "id": "https://ror.org/01gw3d370", - "name": "University of Windsor", - "email_address": "", - "ip_addresses": [ - ], - "established": 1857, + "relationships": [], + "status": "active", "types": [ - "Education" + "education" + ] + }, + { + "admin": { + "created": { + "date": "2022-08-31", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } + }, + "domains": [ + "galgotiacollege.edu" ], - "relationships": [ - - ], - "addresses": [ - { - "lat": 42.305196, - "lng": -83.067483, - "state": "Ontario", - "state_code": "CA-ON", - "city": "Windsor", - "geonames_city": { - "id": 6182962, - "city": "Windsor", - "geonames_admin1": { - "name": "Ontario", - "id": 6093943, - "ascii_name": "Ontario", - "code": "CA.08" - }, - "geonames_admin2": { - "name": null, - "id": null, - "ascii_name": null, - "code": null - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } - }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6251999 + "established": 1999, + "external_ids": [ + { + "all": [ + "0000 0004 1774 2078" + ], + "preferred": "0000 0004 1774 2078", + "type": "isni" } ], + "id": "https://ror.org/04a85ht85", "links": [ - "http://www.uwindsor.ca/" - ], - "aliases": [ - "UWindsor", - "Assumption University of Windsor" + { + "type": "website", + "value": "https://galgotiacollege.edu" + }, + { + "type": "wikipedia", + "value": "https://en.wikipedia.org/wiki/Galgotias_College" + } ], - "acronyms": [ - + "locations": [ + { + "geonames_details": { + "continent_code": "AS", + "continent_name": "Asia", + "country_code": "IN", + "country_name": "India", + "country_subdivision_code": "UP", + "country_subdivision_name": "Uttar Pradesh", + "lat": 28.49615, + "lng": 77.53601, + "name": "Greater Noida" + }, + "geonames_id": 6954929 + } ], - "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Windsor", - "labels": [ + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "GCET" + }, + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "Galgotias College of Engineering & Technology" + }, { - "label": "Université de windsor", - "iso639": "fr" + "lang": "en", + "types": [ + "alias" + ], + "value": "Galgotias College of Engineering and Technology" } ], - "country": { - "country_name": "Canada", - "country_code": "CA" + "relationships": [], + "status": "active", + "types": [ + "education" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "eli-beams.eu" + ], + "established": 2015, + "external_ids": [ + { "all": [ - "0000 0004 1936 9596" - ] + "grid.494603.c" + ], + "preferred": "grid.494603.c", + "type": "grid" }, - "FundRef": { - "preferred": "100009154", + { "all": [ - "100009154", - "501100000083" - ] + "0000 0004 7422 3856" + ], + "preferred": "0000 0004 7422 3856", + "type": "isni" }, - "OrgRef": { - "preferred": null, + { "all": [ - "342733" - ] - }, - "Wikidata": { + "Q39039051" + ], "preferred": null, - "all": [ - "Q2065769" - ] - }, - "GRID": { - "preferred": "grid.267455.7", - "all": "grid.267455.7" + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/02gdzyx04", - "name": "University of Winnipeg", - "email_address": null, - "ip_addresses": [ - ], - "established": 1871, - "types": [ - "Education" + "id": "https://ror.org/00yzpcc69", + "links": [ + { + "type": "website", + "value": "https://www.eli-beams.eu" + } ], - "relationships": [ + "locations": [ { - "label": "Winnipeg Institute for Theoretical Physics", - "type": "Child", - "id": "https://ror.org/010tw2j24" - } - ], - "addresses": [ - { - "lat": 49.890122, - "lng": -97.153367, - "state": "Manitoba", - "state_code": "CA-MB", - "city": "Winnipeg", - "geonames_city": { - "id": 6183235, - "city": "Winnipeg", - "geonames_admin1": { - "name": "Manitoba", - "id": 6065171, - "ascii_name": "Manitoba", - "code": "CA.03" - }, - "geonames_admin2": { - "name": null, - "id": null, - "ascii_name": null, - "code": null - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "EU", + "continent_name": "Europe", + "country_code": "CZ", + "country_name": "Czechia", + "country_subdivision_code": "20", + "country_subdivision_name": "Central Bohemia", + "lat": 49.96321, + "lng": 14.4585, + "name": "Dolní Břežany" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6251999 + "geonames_id": 3076915 } ], - "links": [ - "http://www.uwinnipeg.ca/" - ], - "aliases": [ - - ], - "acronyms": [ - - ], - "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Winnipeg", - "labels": [ + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "ELI-BL" + }, { - "label": "Université de winnipeg", - "iso639": "fr" + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "Extreme Light Infrastructure Beamlines" } ], - "country": { - "country_name": "Canada", - "country_code": "CA" - }, - "external_ids": { - "ISNI": { - "preferred": null, - "all": [ - "0000 0001 1703 4731" - ] + "relationships": [], + "status": "active", + "types": [ + "facility" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" }, - "FundRef": { - "preferred": null, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } + }, + "domains": [], + "established": 1972, + "external_ids": [ + { "all": [ - "100009367" - ] + "grid.430864.d" + ], + "preferred": "grid.430864.d", + "type": "grid" }, - "OrgRef": { - "preferred": null, + { "all": [ - "587404" - ] - }, - "Wikidata": { + "0000 0000 9018 7542" + ], "preferred": null, - "all": [ - "Q472167" - ] - }, - "GRID": { - "preferred": "grid.267457.5", - "all": "grid.267457.5" + "type": "isni" } - } - }, - { - "id": "https://ror.org/03mnm0t94", - "name": "University of Wisconsin–Eau Claire", - "email_address": "", - "ip_addresses": [ - ], - "established": 1916, - "types": [ - "Education" + "id": "https://ror.org/02437s643", + "links": [ + { + "type": "website", + "value": "https://www.uillinois.edu" + } ], - "relationships": [ + "locations": [ { - "label": "University of Wisconsin System", - "type": "Parent", - "id": "https://ror.org/03ydkyb10" - } - ], - "addresses": [ - { - "lat": 44.79895, - "lng": -91.499346, - "state": "Wisconsin", - "state_code": "US-WI", - "city": "Eau Claire", - "geonames_city": { - "id": 5251436, - "city": "Eau Claire", - "geonames_admin1": { - "name": "Wisconsin", - "id": 5279468, - "ascii_name": "Wisconsin", - "code": "US.WI" - }, - "geonames_admin2": { - "name": "Eau Claire County", - "id": 5251439, - "ascii_name": "Eau Claire County", - "code": "US.WI.035" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "NA", + "continent_name": "North America", + "country_code": "US", + "country_name": "United States", + "country_subdivision_code": "IL", + "country_subdivision_name": "Illinois", + "lat": 42.27113, + "lng": -89.094, + "name": "Rockford" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 4907959 } ], - "links": [ - "http://www.uwec.edu/" - ], - "aliases": [ - - ], - "acronyms": [ - "UWEC" + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "UICOMR" + }, + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "University of Illinois Chicago, Rockford campus" + }, + { + "lang": "en", + "types": [ + "alias" + ], + "value": "University of Illinois at Rockford" + } ], - "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Wisconsin%E2%80%93Eau_Claire", - "labels": [ + "relationships": [ + { + "label": "University of Illinois Chicago", + "type": "parent", + "id": "https://ror.org/02mpq6x41" + }, { - "label": "Université du Wisconsin à Eau Claire", - "iso639": "fr" + "label": "Swedish American Hospital", + "type": "related", + "id": "https://ror.org/05scd7d31" } ], - "country": { - "country_name": "United States", - "country_code": "US" + "status": "active", + "types": [ + "education" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "chatham.edu" + ], + "established": 1869, + "external_ids": [ + { "all": [ - "0000 0001 2227 2494" - ] + "grid.411264.4" + ], + "preferred": "grid.411264.4", + "type": "grid" }, - "FundRef": { - "preferred": null, + { "all": [ - "100010315" - ] - }, - "OrgRef": { + "0000 0000 9776 1631" + ], "preferred": null, - "all": [ - "496729" - ] + "type": "isni" }, - "Wikidata": { - "preferred": null, + { "all": [ - "Q3551771" - ] - }, - "GRID": { - "preferred": "grid.267460.1", - "all": "grid.267460.1" + "Q5087708" + ], + "preferred": null, + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/05hbexn54", - "name": "University of Wisconsin–Green Bay", - "email_address": null, - "ip_addresses": [ - ], - "established": 1965, - "types": [ - "Education" + "id": "https://ror.org/05n2dnq32", + "links": [ + { + "type": "website", + "value": "https://www.chatham.edu" + }, + { + "type": "wikipedia", + "value": "http://en.wikipedia.org/wiki/Chatham_University" + } ], - "relationships": [ + "locations": [ { - "label": "University of Wisconsin System", - "type": "Parent", - "id": "https://ror.org/03ydkyb10" - } - ], - "addresses": [ - { - "lat": 44.533203, - "lng": -87.921521, - "state": "Wisconsin", - "state_code": "US-WI", - "city": "Green Bay", - "geonames_city": { - "id": 5254962, - "city": "Green Bay", - "geonames_admin1": { - "name": "Wisconsin", - "id": 5279468, - "ascii_name": "Wisconsin", - "code": "US.WI" - }, - "geonames_admin2": { - "name": "Brown County", - "id": 5246898, - "ascii_name": "Brown County", - "code": "US.WI.009" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "NA", + "continent_name": "North America", + "country_code": "US", + "country_name": "United States", + "country_subdivision_code": "PA", + "country_subdivision_name": "Pennsylvania", + "lat": 40.44062, + "lng": -79.99589, + "name": "Pittsburgh" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 5206379 } ], - "links": [ - "http://www.uwgb.edu/" - ], - "aliases": [ - - ], - "acronyms": [ - "UWGB" - ], - "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Wisconsin%E2%80%93Green_Bay", - "labels": [ + "names": [ { - "label": "Université du Wisconsin–Green Bay", - "iso639": "fr" + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "Chatham University" } ], - "country": { - "country_name": "United States", - "country_code": "US" + "relationships": [], + "status": "active", + "types": [ + "education" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "lds.no" + ], + "established": 1992, + "external_ids": [ + { "all": [ - "0000 0001 0559 7692" - ] + "501100010678" + ], + "preferred": "501100010678", + "type": "fundref" }, - "OrgRef": { - "preferred": null, + { "all": [ - "1513886" - ] + "grid.416137.6" + ], + "preferred": "grid.416137.6", + "type": "grid" }, - "Wikidata": { - "preferred": null, + { "all": [ - "Q2378091" - ] - }, - "GRID": { - "preferred": "grid.267461.0", - "all": "grid.267461.0" + "0000 0004 0627 3157" + ], + "preferred": null, + "type": "isni" } - } - }, - { - "id": "https://ror.org/00x8ccz20", - "name": "University of Wisconsin–La Crosse", - "email_address": "", - "ip_addresses": [ - ], - "established": 1909, - "types": [ - "Education" + "id": "https://ror.org/03ym7ve89", + "links": [ + { + "type": "website", + "value": "https://www.lovisenbergsykehus.no" + } ], - "relationships": [ + "locations": [ { - "label": "University of Wisconsin System", - "type": "Parent", - "id": "https://ror.org/03ydkyb10" - } - ], - "addresses": [ - { - "lat": 43.815576, - "lng": -91.233517, - "state": "Wisconsin", - "state_code": "US-WI", - "city": "La Crosse", - "geonames_city": { - "id": 5258957, - "city": "La Crosse", - "geonames_admin1": { - "name": "Wisconsin", - "id": 5279468, - "ascii_name": "Wisconsin", - "code": "US.WI" - }, - "geonames_admin2": { - "name": "La Crosse County", - "id": 5258961, - "ascii_name": "La Crosse County", - "code": "US.WI.063" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "EU", + "continent_name": "Europe", + "country_code": "NO", + "country_name": "Norway", + "country_subdivision_code": "03", + "country_subdivision_name": "Oslo", + "lat": 59.91273, + "lng": 10.74609, + "name": "Oslo" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 3143244 } ], - "links": [ - "http://www.uwlax.edu/Home/Future-Students/" - ], - "aliases": [ - - ], - "acronyms": [ - "UW–L" - ], - "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Wisconsin%E2%80%93La_Crosse", - "labels": [ + "names": [ { - "label": "Université du Wisconsin–La Crosse", - "iso639": "fr" + "lang": "no", + "types": [ + "ror_display", + "label" + ], + "value": "Lovisenberg Diakonale Sykehus" } ], - "country": { - "country_name": "United States", - "country_code": "US" + "relationships": [], + "status": "active", + "types": [ + "funder", + "healthcare" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "uim-makassar.ac.id" + ], + "established": 2000, + "external_ids": [ + { "all": [ - "0000 0001 2169 5137" - ] + "grid.443680.d" + ], + "preferred": "grid.443680.d", + "type": "grid" }, - "OrgRef": { - "preferred": null, + { "all": [ - "2422287" - ] - }, - "Wikidata": { + "0000 0001 0588 5299" + ], "preferred": null, + "type": "isni" + }, + { "all": [ - "Q2688358" - ] + "Q12523343" + ], + "preferred": null, + "type": "wikidata" + } + ], + "id": "https://ror.org/05baqgp89", + "links": [ + { + "type": "website", + "value": "https://uim-makassar.ac.id/" + } + ], + "locations": [ + { + "geonames_details": { + "continent_code": "AS", + "continent_name": "Asia", + "country_code": "ID", + "country_name": "Indonesia", + "country_subdivision_code": "SN", + "country_subdivision_name": "South Sulawesi", + "lat": -5.14861, + "lng": 119.43194, + "name": "Makassar" + }, + "geonames_id": 1622786 + } + ], + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "UIM" }, - "GRID": { - "preferred": "grid.267462.3", - "all": "grid.267462.3" + { + "lang": "id", + "types": [ + "ror_display", + "label" + ], + "value": "Universitas Islam Makassar" } - } + ], + "relationships": [], + "status": "active", + "types": [ + "education" + ] } ], "meta": { "types": [ { "id": "company", - "title": "Company", - "count": 29790 + "title": "company", + "count": 30791 }, { "id": "education", - "title": "Education", - "count": 20325 + "title": "education", + "count": 22599 + }, + { + "id": "funder", + "title": "funder", + "count": 17078 }, { "id": "nonprofit", - "title": "Nonprofit", - "count": 14187 + "title": "nonprofit", + "count": 15641 }, { "id": "healthcare", - "title": "Healthcare", - "count": 13107 + "title": "healthcare", + "count": 14062 }, { "id": "facility", - "title": "Facility", - "count": 10080 + "title": "facility", + "count": 12758 }, { "id": "other", - "title": "Other", - "count": 8369 + "title": "other", + "count": 9026 }, { "id": "government", - "title": "Government", - "count": 6511 + "title": "government", + "count": 7599 }, { "id": "archive", - "title": "Archive", - "count": 2967 + "title": "archive", + "count": 3104 } ], "countries": [ { "id": "us", "title": "United States", - "count": 31196 + "count": 32118 }, { "id": "gb", "title": "United Kingdom", - "count": 7410 + "count": 7581 }, { - "id": "de", - "title": "Germany", - "count": 5189 + "id": "jp", + "title": "Japan", + "count": 5754 }, { - "id": "cn", - "title": "China", - "count": 4846 + "id": "de", + "title": "Germany", + "count": 5372 }, { "id": "fr", "title": "France", - "count": 4344 + "count": 5110 }, { - "id": "jp", - "title": "Japan", - "count": 3940 + "id": "cn", + "title": "China", + "count": 5001 }, { "id": "ca", "title": "Canada", - "count": 3392 + "count": 3610 }, { "id": "in", "title": "India", - "count": 3075 + "count": 3399 }, { "id": "cz", "title": "Czech Republic", - "count": 2780 + "count": 2843 + }, + { + "id": "it", + "title": "Italy", + "count": 2196 + } + ], + "continents": [ + { + "id": "eu", + "title": "Europe", + "count": 45322 + }, + { + "id": "na", + "title": "North America", + "count": 37230 + }, + { + "id": "as", + "title": "Asia", + "count": 23498 + }, + { + "id": "af", + "title": "Africa", + "count": 3835 + }, + { + "id": "sa", + "title": "South America", + "count": 3583 + }, + { + "id": "oc", + "title": "Oceania", + "count": 1945 }, { - "id": "ru", - "title": "Russia", - "count": 2109 + "id": "an", + "title": "Antarctica", + "count": 2 } ], "statuses": [ { "id": "active", "title": "active", - "count": 105336 + "count": 115409 } ] } -} +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/sword2/example-embargo.zip b/dspace-server-webapp/src/test/resources/org/dspace/app/sword2/example-embargo.zip new file mode 100644 index 000000000000..ff1273679b88 Binary files /dev/null and b/dspace-server-webapp/src/test/resources/org/dspace/app/sword2/example-embargo.zip differ diff --git a/dspace-services/pom.xml b/dspace-services/pom.xml index 91cb7088eb47..995c03f08a00 100644 --- a/dspace-services/pom.xml +++ b/dspace-services/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT @@ -90,13 +90,6 @@ spring-context-support ${spring.version} compile - - - - org.springframework - spring-jcl - - org.apache.commons diff --git a/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationPlaceholderConfigurer.java b/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationPlaceholderConfigurer.java index caa715e21bfb..b85450dcd039 100644 --- a/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationPlaceholderConfigurer.java +++ b/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationPlaceholderConfigurer.java @@ -8,6 +8,7 @@ package org.dspace.servicemanager.config; import org.apache.commons.configuration2.Configuration; +import org.apache.commons.configuration2.spring.ConfigurationPropertySource; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.core.env.MutablePropertySources; @@ -26,8 +27,8 @@ public class DSpaceConfigurationPlaceholderConfigurer extends PropertySourcesPlaceholderConfigurer { public DSpaceConfigurationPlaceholderConfigurer(Configuration configuration) { - DSpaceConfigurationPropertySource apacheCommonsConfigPropertySource = - new DSpaceConfigurationPropertySource(configuration.getClass().getName(), configuration); + ConfigurationPropertySource apacheCommonsConfigPropertySource = + new ConfigurationPropertySource(configuration.getClass().getName(), configuration); MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addLast(apacheCommonsConfigPropertySource); setPropertySources(propertySources); diff --git a/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationPropertySource.java b/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationPropertySource.java deleted file mode 100644 index d3394399301f..000000000000 --- a/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationPropertySource.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.dspace.servicemanager.config; - -import java.util.ArrayList; -import java.util.List; - -import org.apache.commons.configuration2.Configuration; -import org.apache.commons.lang3.ArrayUtils; -import org.springframework.core.env.EnumerablePropertySource; - -/** - * Allow use of Apache Commons Configuration Objects as Spring PropertySources. - * This class is a temporary copy of the ConfigurationPropertySource class in the Apache Commons Configuration - * project needed until to fix the issue https://issues.apache.org/jira/browse/CONFIGURATION-846 - */ -public class DSpaceConfigurationPropertySource extends EnumerablePropertySource { - - protected DSpaceConfigurationPropertySource(final String name) { - super(name); - } - - public DSpaceConfigurationPropertySource(final String name, final Configuration source) { - super(name, source); - } - - @Override - public Object getProperty(final String name) { - if (source.getProperty(name) != null) { - final String[] propValue = source.getStringArray(name); - if (propValue == null || propValue.length == 0) { - return ""; - } else if (propValue.length == 1) { - return propValue[0]; - } else { - return propValue; - } - } else { - return null; - } - } - - @Override - public String[] getPropertyNames() { - final List keys = new ArrayList<>(); - source.getKeys().forEachRemaining(keys::add); - return keys.toArray(ArrayUtils.EMPTY_STRING_ARRAY); - } -} diff --git a/dspace-services/src/main/java/org/dspace/services/ConfigurationService.java b/dspace-services/src/main/java/org/dspace/services/ConfigurationService.java index 526a518a0911..7681553d4168 100644 --- a/dspace-services/src/main/java/org/dspace/services/ConfigurationService.java +++ b/dspace-services/src/main/java/org/dspace/services/ConfigurationService.java @@ -251,6 +251,8 @@ public interface ConfigurationService { * Set a configuration property (setting) in the system. * Type is not important here since conversion happens automatically * when properties are requested. + *
+ * Note: use with care, the value will be reset when the configuration is reloaded! * * @param name the property name * @param value the property value (set this to null to clear out the property) diff --git a/dspace-services/src/main/java/org/dspace/services/email/EmailServiceImpl.java b/dspace-services/src/main/java/org/dspace/services/email/EmailServiceImpl.java index b8de1c79994a..56335c1a542d 100644 --- a/dspace-services/src/main/java/org/dspace/services/email/EmailServiceImpl.java +++ b/dspace-services/src/main/java/org/dspace/services/email/EmailServiceImpl.java @@ -62,6 +62,7 @@ public Session getSession() { } @PostConstruct + // JNDI usage is safe here as it looks up a configured mail session resource, not user input. @SuppressWarnings("BanJNDI") public void init() { // See if there is already a Session in our environment diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index 848e7496e060..0a845b03b183 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT .. @@ -46,11 +46,6 @@ org.springframework.boot spring-boot-starter-logging - - - org.springframework - spring-jcl -
@@ -77,7 +72,7 @@ xom xom - 1.3.9 + 1.4.1 diff --git a/dspace-swordv2/pom.xml b/dspace-swordv2/pom.xml index 8767dd9bb6a5..c2296ecb97b4 100644 --- a/dspace-swordv2/pom.xml +++ b/dspace-swordv2/pom.xml @@ -13,7 +13,7 @@ org.dspace dspace-parent - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT .. @@ -83,11 +83,6 @@ org.springframework.boot spring-boot-starter-logging - - - org.springframework - spring-jcl - diff --git a/dspace-swordv2/src/main/java/org/dspace/sword2/CollectionDepositManagerDSpace.java b/dspace-swordv2/src/main/java/org/dspace/sword2/CollectionDepositManagerDSpace.java index 7f77c00f0d9b..afc35cfaa11c 100644 --- a/dspace-swordv2/src/main/java/org/dspace/sword2/CollectionDepositManagerDSpace.java +++ b/dspace-swordv2/src/main/java/org/dspace/sword2/CollectionDepositManagerDSpace.java @@ -151,6 +151,9 @@ public DepositReceipt createNew(String collectionUri, Deposit deposit, sc.commit(); return receipt; + } catch (SwordAuthException e) { + log.error("caught exception:", e); + throw e; } catch (DSpaceSwordException e) { log.error("caught exception:", e); throw new SwordServerException( diff --git a/dspace-swordv2/src/main/java/org/dspace/sword2/SwordAuthenticator.java b/dspace-swordv2/src/main/java/org/dspace/sword2/SwordAuthenticator.java index 54b769388c68..e47c0f076b98 100644 --- a/dspace-swordv2/src/main/java/org/dspace/sword2/SwordAuthenticator.java +++ b/dspace-swordv2/src/main/java/org/dspace/sword2/SwordAuthenticator.java @@ -9,6 +9,7 @@ import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; import java.util.List; @@ -618,31 +619,33 @@ public List getAllowedCollections( // short cut by obtaining the collections to which the authenticated user can submit List cols = collectionService.findAuthorized( - authContext, community, Constants.ADD); + authContext, community, Arrays.asList(Constants.ADD, Constants.ADMIN)); + List allowed = new ArrayList<>(); // now find out if the obo user is allowed to submit to any of these collections - for (Collection col : cols) { - boolean oboAllowed = false; - - // check for obo null - if (swordContext.getOnBehalfOf() == null) { - oboAllowed = true; - } - - // if we have not already determined that the obo user is ok to submit, look up the READ policy on the - // community. THis will include determining if the user is an administrator. - if (!oboAllowed) { - oboAllowed = authorizeService.authorizeActionBoolean( - swordContext.getOnBehalfOfContext(), col, - Constants.ADD); - } + if (swordContext.getOnBehalfOf() != null) { + for (Collection col : cols) { + boolean oboAllowed = false; + + //if we have not already determined that the obo user is ok to submit, + //look up the READ policy on the + // community. THis will include determining if the user is an administrator. + if (!oboAllowed) { + oboAllowed = authorizeService.authorizeActionBoolean( + swordContext.getOnBehalfOfContext(), col, + Constants.ADD); + } - // final check to see if we are allowed to READ - if (oboAllowed) { - allowed.add(col); + // final check to see if we are allowed to READ + if (oboAllowed) { + allowed.add(col); + } } + } else { + return cols; } + return allowed; } catch (SQLException e) { diff --git a/dspace-swordv2/src/main/java/org/dspace/sword2/SwordMETSContentIngester.java b/dspace-swordv2/src/main/java/org/dspace/sword2/SwordMETSContentIngester.java index a77caa655b5d..45f0f315aa28 100644 --- a/dspace-swordv2/src/main/java/org/dspace/sword2/SwordMETSContentIngester.java +++ b/dspace-swordv2/src/main/java/org/dspace/sword2/SwordMETSContentIngester.java @@ -10,10 +10,12 @@ import java.io.File; import org.apache.logging.log4j.Logger; +import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; import org.dspace.content.WorkspaceItem; +import org.dspace.content.crosswalk.CrosswalkException; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.packager.PackageIngester; import org.dspace.content.packager.PackageParameters; @@ -28,6 +30,7 @@ import org.swordapp.server.SwordAuthException; import org.swordapp.server.SwordError; import org.swordapp.server.SwordServerException; +import org.swordapp.server.UriRegistry; public class SwordMETSContentIngester extends AbstractSwordContentIngester { /** @@ -181,6 +184,12 @@ public DepositResult ingestToCollection(Context context, Deposit deposit, } catch (RuntimeException re) { log.error("caught exception: ", re); throw re; + } catch (AuthorizeException e) { + log.error("caught exception: ", e); + throw new SwordAuthException(e); + } catch (CrosswalkException e) { + log.error("caught exception: ", e); + throw new SwordError(UriRegistry.ERROR_BAD_REQUEST, e.getMessage(), e); } catch (Exception e) { log.error("caught exception: ", e); throw new DSpaceSwordException(e); diff --git a/dspace-swordv2/src/main/java/org/dspace/sword2/SwordUrlManager.java b/dspace-swordv2/src/main/java/org/dspace/sword2/SwordUrlManager.java index eee3627c4045..4ba0f9950e11 100644 --- a/dspace-swordv2/src/main/java/org/dspace/sword2/SwordUrlManager.java +++ b/dspace-swordv2/src/main/java/org/dspace/sword2/SwordUrlManager.java @@ -7,6 +7,8 @@ */ package org.dspace.sword2; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.util.List; @@ -107,7 +109,7 @@ public String getSwordBaseUrl() "Unable to construct service document urls, due to missing/invalid " + "config in sword2.url and/or dspace.server.url"); } - sUrl = buildSWORDUrl("swordv2"); + sUrl = dspaceUrl + "/" + swordPath; } return sUrl; } @@ -386,10 +388,10 @@ public String getBitstreamUrl(Bitstream bitstream) if (handle != null && !"".equals(handle)) { bsLink = bsLink + "/bitstream/" + handle + "/" + - bitstream.getSequenceID() + "/" + bitstream.getName(); + bitstream.getSequenceID() + "/" + URLEncoder.encode(bitstream.getName(), StandardCharsets.UTF_8); } else { bsLink = bsLink + "/retrieve/" + bitstream.getID() + "/" + - bitstream.getName(); + URLEncoder.encode(bitstream.getName(), StandardCharsets.UTF_8); } return bsLink; @@ -401,7 +403,7 @@ public String getBitstreamUrl(Bitstream bitstream) public String getActionableBitstreamUrl(Bitstream bitstream) throws DSpaceSwordException { return this.getSwordBaseUrl() + "/edit-media/bitstream/" + - bitstream.getID() + "/" + bitstream.getName(); + bitstream.getID() + "/" + URLEncoder.encode(bitstream.getName(), StandardCharsets.UTF_8); } public boolean isActionableBitstreamUrl(Context context, String url) { diff --git a/dspace/config/crosswalks/DIM2DataCite.xsl b/dspace/config/crosswalks/DIM2DataCite.xsl index 935b3dc4038a..34fb9c64bf96 100644 --- a/dspace/config/crosswalks/DIM2DataCite.xsl +++ b/dspace/config/crosswalks/DIM2DataCite.xsl @@ -350,16 +350,23 @@ + + + + + + + @@ -642,4 +649,19 @@ + + + + + + https://orcid.org/ + ORCID + + + + + diff --git a/dspace/config/crosswalks/DIM2UmdDataCite.xsl b/dspace/config/crosswalks/DIM2UmdDataCite.xsl index 37153bd3a079..bf4746b67f34 100644 --- a/dspace/config/crosswalks/DIM2UmdDataCite.xsl +++ b/dspace/config/crosswalks/DIM2UmdDataCite.xsl @@ -344,7 +344,8 @@ company as well. We have to ensure to use URIs of our prefix as primary identifiers only. --> - + + @@ -352,15 +353,23 @@ + + + + + + + + @@ -649,4 +658,19 @@ + + + + + + https://orcid.org/ + ORCID + + + + + diff --git a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl index fdf93da5ae31..572af5278a3e 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl @@ -5,7 +5,7 @@ detailed in the LICENSE and NOTICE files at the root of the source tree and available online at - Developed by Paulo Graça + Developed by paulo-graca > https://www.openaire.eu/schema/repo-lit/4.0/openaire.xsd @@ -101,7 +101,7 @@ - + @@ -137,7 +137,7 @@ - + @@ -206,7 +206,7 @@ - + @@ -303,7 +303,7 @@ - @@ -369,7 +369,7 @@ + schemeURI="https://www.webofscience.com"> @@ -406,7 +406,7 @@ - + @@ -489,7 +489,7 @@ - + @@ -599,7 +599,7 @@ - + @@ -618,7 +618,7 @@ - + @@ -640,7 +640,7 @@ - + @@ -670,7 +670,7 @@ - + @@ -704,7 +704,7 @@ - + @@ -724,8 +724,8 @@ - - + + @@ -736,7 +736,7 @@ - + @@ -746,7 +746,7 @@ @@ -779,7 +779,7 @@ - + @@ -791,7 +791,7 @@ - + @@ -802,8 +802,8 @@ - - + + @@ -827,7 +827,7 @@ - + @@ -840,7 +840,7 @@ - + @@ -856,15 +856,15 @@ - + - - + + - - + + @@ -875,7 +875,7 @@ - + @@ -945,7 +945,7 @@ - + @@ -954,7 +954,7 @@ - + @@ -963,7 +963,7 @@ - + @@ -972,7 +972,7 @@ - + @@ -981,7 +981,7 @@ - + @@ -990,7 +990,7 @@ - + @@ -999,7 +999,7 @@ - + @@ -1009,7 +1009,7 @@ - + @@ -1071,7 +1071,7 @@ - + @@ -1083,7 +1083,7 @@ - + Available @@ -1425,7 +1425,7 @@ @@ -1461,7 +1461,7 @@ literature - + dataset @@ -1478,7 +1478,7 @@ This template will return the COAR Resource Type Vocabulary URI like http://purl.org/coar/resource_type/c_6501 based on a valued text like 'article' - https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/v4.0.0/field_publicationtype.html#attribute-uri-m + https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/4.0.1/field_publicationtype.html#attribute-uri-m --> @@ -1674,7 +1674,7 @@ like "open access" based on the values from DSpace Access Status mechanism like String 'open.access' please check class org.dspace.access.status.DefaultAccessStatusHelper for more information - https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/v4.0.0/field_accessrights.html#definition-and-usage-instruction + https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/4.0.1/field_accessrights.html#definition-and-usage-instruction --> @@ -1704,7 +1704,7 @@ This template will return the COAR Access Right Vocabulary URI like http://purl.org/coar/access_right/c_abf2 based on a value text like 'open access' - https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/v4.0.0/field_accessrights.html#definition-and-usage-instruction + https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/4.0.1/field_accessrights.html#definition-and-usage-instruction --> diff --git a/dspace/config/crosswalks/oai/transformers/openaire4.xsl b/dspace/config/crosswalks/oai/transformers/openaire4.xsl index 8ac703609d6f..7f26b81c344a 100644 --- a/dspace/config/crosswalks/oai/transformers/openaire4.xsl +++ b/dspace/config/crosswalks/oai/transformers/openaire4.xsl @@ -12,8 +12,8 @@ @@ -80,7 +80,7 @@ Normalizing dc.rights according to COAR Controlled Vocabulary for Access Rights (Version 1.0) (http://vocabularies.coar-repositories.org/documentation/access_rights/) available at - https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/v4.0.0/field_accessrights.html#definition-and-usage-instruction + https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/4.0.1/field_accessrights.html#definition-and-usage-instruction --> @@ -116,7 +116,7 @@ diff --git a/dspace/config/crosswalks/oai/xoai.xml b/dspace/config/crosswalks/oai/xoai.xml index 0f1cdf7d68cd..da51eb5eaf77 100644 --- a/dspace/config/crosswalks/oai/xoai.xml +++ b/dspace/config/crosswalks/oai/xoai.xml @@ -74,7 +74,7 @@ - This contexts complies with Openaire Guidelines for Literature Repositories v4.0. + This contexts complies with OpenAIRE Guidelines for Institutional and Thematic Repository Managers v4.0. oai_openaire diff --git a/dspace/config/crosswalks/orcid/mapConverter-dspace-to-orcid-publication-type.properties b/dspace/config/crosswalks/orcid/mapConverter-dspace-to-orcid-publication-type.properties index 953ddc60eef6..a45465b08398 100644 --- a/dspace/config/crosswalks/orcid/mapConverter-dspace-to-orcid-publication-type.properties +++ b/dspace/config/crosswalks/orcid/mapConverter-dspace-to-orcid-publication-type.properties @@ -4,20 +4,21 @@ Article = journal-article Book = book Book\ chapter = book-chapter Dataset = data-set -Learning\ Object = other -Image = other -Image,\ 3-D = other -Map = other -Musical\ Score = other +Learning\ Object = learning-object +Image = image +Image,\ 3-D = image +Journal = journal-issue +Map = cartographic-material +Musical\ Score = musical-composition Plan\ or\ blueprint = other Preprint = preprint Presentation = other -Recording,\ acoustical = other -Recording,\ musical = other -Recording,\ oral = other +Recording,\ acoustical = sound +Recording,\ musical = sound +Recording,\ oral = sound Software = software -Technical\ Report = other -Thesis = other -Video = other +Technical\ Report = report +Thesis = dissertation-thesis +Video = moving-image Working\ Paper = working-paper -Other = other \ No newline at end of file +Other = other diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 5b44b01ddd51..56a5442d37b4 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -162,6 +162,7 @@ mail.from.address = dspace-noreply@myu.edu # will use the above settings to create a Session. #mail.session.name = Session + # When feedback is submitted via the Feedback form, it is sent to this address # Currently limited to one recipient! # if this property is empty or commented out, feedback form is disabled @@ -228,6 +229,22 @@ mail.message.headers = charset # Helpdesk telephone. Not email, but should be with other contact info. Optional. #mail.message.helpdesk.telephone = +1 555 555 5555 +# Allowed configuration properties, to pass in a "config" map to email and LDN templates. +# This allows templates to easily access dynamic configuration properties, without +# exposing sensitive information to the templating engine +message.templates.allowed-config = dspace.name +message.templates.allowed-config = dspace.shortname +message.templates.allowed-config = dspace.ui.url +message.templates.allowed-config = mail.helpdesk +message.templates.allowed-config = mail.message.helpdesk.telephone +message.templates.allowed-config = mail.admin +message.templates.allowed-config = mail.admin.name + +# Whether to run Velocity in strict mode (null parameter values in templates for LDN or Email will result +# in an Exception instead of a blank string) +# Default: false (this can introduce unwanted side-effects if e.g. a submitter eperson is deleted for a workflow task) +#message.templates.strict_mode = false + ##### Asset Storage (bitstreams / files) ###### # Moved to config/spring/api/bitstore.xml @@ -954,6 +971,7 @@ registry.metadata.load = dspace-types.xml registry.metadata.load = iiif-types.xml registry.metadata.load = datacite-types.xml registry.metadata.load = coar-types.xml +registry.metadata.load = journal-types.xml #---------------------------------------------------------------# #-----------------UI-Related CONFIGURATIONS---------------------# @@ -1003,9 +1021,11 @@ metadata.hide.person.email = true #webui.submit.upload.required = true # Which field should be used for type-bind -# Defaults to 'dc.type'; If changing this value, you must also update the related -# dspace-angular environment configuration property submission.typeBind.field -#submit.type-bind.field = dc.type +# If changing this value, you must also update the related +# dspace-angular environment configuration property submission.typeBind.field. +# This property should not be left commented out, or the REST configuration +# exposure will fail +submit.type-bind.field = dc.type #### Creative Commons settings ###### @@ -1390,21 +1410,21 @@ websvc.opensearch.max_num_of_items_per_request = 100 #### Content Inline Disposition Threshold #### # -# Set the max size of a bitstream that can be served inline -# Use -1 to force all bitstream to be served inline -webui.content_disposition_threshold = 8388608 - -#### Content Attachment Disposition Formats #### -# -# Set which mimetypes or file extensions will NOT be opened inline. -# Files with these mimetypes/extensions will always be downloaded, regardless of the threshold above. +# Set which mimetypes or file extensions are allowed to be opened inline in a user's browser. +# By default, all files will be downloaded, regardless of the threshold below, unless specified in this configuration. # NOTE: For security reasons, some file formats (e.g. HTML, XML, RDF, JS) will always be downloaded regardless -# of the settings here. This blocks these formats from executing embedded JavaScript when opened inline. -# For additional security, you may choose to set this to "*" to force all formats to always be downloaded -# (i.e. disables all formats from opening inline within the user's browser). +# of the settings here. This blocks these formats from executing embedded JavaScript when opened inline, protecting +# the site from potential XSS attacks. # -# By default, RTF is always downloaded because most browsers attempt to display it as plain text. -webui.content_disposition_format = text/richtext +# For example: this setting defaults to enabling PDF and common image / audio / video formats to be opened +# (or potentially streamed) in a user's browser. +webui.content_disposition_inline = application/pdf, image/gif, image/jpeg, image/png, audio/mpeg, video/mpeg, video/mp4 + +# +# Set the max size (in bytes) of a bitstream that can be served inline. This setting only applies to formats +# specified in the "webui.content_disposition_inline" configuration above. +# Default = 8MB (8388608 bytes). Use -1 to ignore the size of file when serving it inline. +#webui.content_disposition_threshold = 8388608 #### Multi-file HTML document/site settings ##### # TODO: UNSUPPORTED in DSpace 7.0. May be re-added in a later release @@ -1564,12 +1584,6 @@ request.item.helpdesk.override = false # Setting it to "false" results in a silent rejection. request.item.reject.email = true -#------------------------------------------------------------------# -#------------------SUBMISSION CONFIGURATION------------------------# -#------------------------------------------------------------------# -# Field to use for type binding, default dc.type -submit.type-bind.field = dc.type - #---------------------------------------------------------------# #----------SOLR DATABASE RESYNC SCRIPT CONFIGURATION------------# #---------------------------------------------------------------# @@ -1655,7 +1669,6 @@ include = ${module_dir}/authentication-ldap.cfg include = ${module_dir}/authentication-oidc.cfg include = ${module_dir}/authentication-password.cfg include = ${module_dir}/authentication-shibboleth.cfg -include = ${module_dir}/authentication-x509.cfg include = ${module_dir}/authority.cfg include = ${module_dir}/bulkedit.cfg include = ${module_dir}/citation-page.cfg diff --git a/dspace/config/dstat.map b/dspace/config/dstat.map index 140049ee13a1..bfd08ede3c56 100644 --- a/dspace/config/dstat.map +++ b/dspace/config/dstat.map @@ -100,4 +100,8 @@ show_feedback_form=Feedback Form Displayed create_dc_type=New Dublin Core Type Created remove_template_item=Item Template Removed withdraw_item=Item Withdrawn -download_export_archive = Download Export Archive \ No newline at end of file +download_export_archive = Download Export Archive +add_group_eperson = EPerson Added to Group +remove_group_eperson = EPerson Removed from Group +add_group_subgroup = Child Group Added to Group +remove_group_subgroup = Child Group Removed from Group \ No newline at end of file diff --git a/dspace/config/entities/openaire4-relationships.xml b/dspace/config/entities/openaire4-relationships.xml index daa0e2c1da9c..d77371b603a4 100644 --- a/dspace/config/entities/openaire4-relationships.xml +++ b/dspace/config/entities/openaire4-relationships.xml @@ -3,7 +3,7 @@ - Publication @@ -17,7 +17,7 @@ 0 - Publication @@ -31,7 +31,7 @@ 0 - Publication @@ -45,7 +45,7 @@ 0 - Publication @@ -59,7 +59,7 @@ 0 - Publication @@ -73,7 +73,7 @@ 0 - Project diff --git a/dspace/config/hibernate-ehcache-config.xml b/dspace/config/hibernate-ehcache-config.xml index e2edf67b602e..680211a9acdc 100644 --- a/dspace/config/hibernate-ehcache-config.xml +++ b/dspace/config/hibernate-ehcache-config.xml @@ -145,4 +145,27 @@ + + + + 1 + + + 500 + + + + + + + 1 + + + 100 + + + diff --git a/dspace/config/local.cfg.EXAMPLE b/dspace/config/local.cfg.EXAMPLE index 358336a6f77c..ea9d8b1dfbe0 100644 --- a/dspace/config/local.cfg.EXAMPLE +++ b/dspace/config/local.cfg.EXAMPLE @@ -255,9 +255,6 @@ plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authen # ORCID certificate authentication. # plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.OrcidAuthentication -# X.509 certificate authentication. See authentication-x509.cfg for default configuration. -#plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.X509Authentication - # Authentication by Password (encrypted in DSpace's database). See authentication-password.cfg for default configuration. # Enabled by default in authentication.cfg # UMD Customization diff --git a/dspace/config/migration/item-submissions.xsl b/dspace/config/migration/item-submissions.xsl index 9b1de738e1fd..aed95992ab17 100644 --- a/dspace/config/migration/item-submissions.xsl +++ b/dspace/config/migration/item-submissions.xsl @@ -34,35 +34,44 @@ configuration file into a DSpace 7.x (or above) item-submission.xml --> - - - - - - - - - - - + + + + + + + + + + + + org.dspace.app.rest.submit.step.CollectionStep + + + + + + + + + + + + + submission-form + + + - - + + submission + + + submission - - submission-form - - - - - submission - - - submission - - - + + diff --git a/dspace/config/modules/assetstore.cfg b/dspace/config/modules/assetstore.cfg index cbee6bd2c3a4..5951e291e950 100644 --- a/dspace/config/modules/assetstore.cfg +++ b/dspace/config/modules/assetstore.cfg @@ -12,12 +12,15 @@ assetstore.dir = ${dspace.dir}/assetstore # This value will be used as `incoming` default store inside the `bitstore.xml` # Possible values are: # - 0: to use the `localStore`; -# - 1: to use the `s3Store`. +# - 1: to use the `s3Store`. # If you want to add additional assetstores, they must be added to that bitstore.xml # and new values should be provided as key-value pairs in the `stores` map of the -# `bitstore.xml` configuration. +# `bitstore.xml` configuration. assetstore.index.primary = 0 +#if the assetstore path is symbolic link, use this configuration to allow that path. +#assetstore.allowed.roots = /data/assetstore + #---------------------------------------------------------------# #-------------- Amazon S3 Specific Configurations --------------# #---------------------------------------------------------------# @@ -44,6 +47,9 @@ assetstore.s3.bucketName = # is shared. Optional, default is root level of bucket assetstore.s3.subfolder = +# Optional custom S3 endpoint URI. Leave this blank / commented to use the Amazon default +# assetstore.s3.endpoint = + # please don't use root credentials in production but rely on the aws credentials default # discovery mechanism to configure them (ENV VAR, EC2 Iam role, etc.) # The preferred approach for security reason is to use the IAM user credentials, but isn't always possible. @@ -54,4 +60,34 @@ assetstore.s3.awsSecretKey = # If the credentials are left empty, # then this setting is ignored and the default AWS region will be used. -assetstore.s3.awsRegionName = \ No newline at end of file +assetstore.s3.awsRegionName = + +# The target throughput for transfer requests in Gbps. Higher value means more connections will be established with S3. +assetstore.s3.targetThroughputGbps = 10.0 + +# Sets the minimum part size for transfer parts. +# Decreasing the minimum part size causes multipart transfer to be split into a larger number of smaller parts. +# Increase also the number of maxConcurrency if you want to maxout the throughput with smaller part sizes. +assetstore.s3.minPartSizeBytes = 8388608 + +# Specifies the maximum number of S3 connections that should be established during a transfer. +# If not provided, it will be used a proper value depending on the machine capabilities (CPU cores and available memory). +# assetstore.s3.maxConcurrency = + +# The amount of maximum memory used during data transfers, as a percentage of the total available memory. +# Default is 0.1 (10% of total memory - Xmx). Adjust this value based on the available memory and expected transfer sizes. +# This value should be set between 0 and 1, where 0 means no memory usage limit and 1 means using all available memory. +# BE careful with this value, since it can cause OOM - especially for lower Xmx values ( 256m / 512m ) +# If not set, there is no memory usage limit and the S3 client will use as much memory as needed for transfers, +# which may lead to OutOfMemory errors if the transfer sizes are large and the available memory is limited. +assetstore.s3.memoryUsageFactor=0.1 + + +# The size of the buffer used for reading data from S3 during transfers, in bytes. +# Default is 16KB (16384 bytes). Adjust this value based on the expected transfer sizes and available memory. +# A larger buffer size may improve performance for larger transfers, but it also increases memory usage. +# If not set, the S3 client will use a default buffer size of 16KB (16384 bytes) for reading data from S3 during transfers. +#assetstore.s3.initialReadBufferSizeInBytes= + +# The algorithm the S3 client will use to create a checksum when doing putObject. +assetstore.s3.s3ChecksumAlgorithm = CRC32 diff --git a/dspace/config/modules/authentication-x509.cfg b/dspace/config/modules/authentication-x509.cfg deleted file mode 100644 index d3f05c7d17d5..000000000000 --- a/dspace/config/modules/authentication-x509.cfg +++ /dev/null @@ -1,23 +0,0 @@ -#---------------------------------------------------------------# -#------X.509 CERTIFICATE AUTHENTICATION CONFIGURATIONS----------# -#---------------------------------------------------------------# -# Configuration properties used by the X.509 Certificate # -# Authentication plugin, when it is enabled. # -#---------------------------------------------------------------# - -## method 1, using keystore -#authentication-x509.keystore.path = /tomcat/conf/keystore -#authentication-x509.keystore.password = changeit - -## method 2, using CA certificate -#authentication-x509.ca.cert = ${dspace.dir}/config/MyClientCA.pem - -## Create e-persons for unknown names in valid certificates? -#authentication-x509.autoregister = true - -## Allow Certificate auth to show as a choice in chooser -# Use Messages.properties key for title -#authentication-x509.chooser.title.key=org.dspace.eperson.X509Authentication.title -# -# Identify the location of the Certificate Login Servlet. -#authentication-x509.chooser.uri=/certificate-login diff --git a/dspace/config/modules/authentication.cfg b/dspace/config/modules/authentication.cfg index 568f871e3cd7..253035fe3e52 100644 --- a/dspace/config/modules/authentication.cfg +++ b/dspace/config/modules/authentication.cfg @@ -21,9 +21,6 @@ # * IP Address Authentication # Plugin class: org.dspace.authenticate.IPAuthentication # Configuration file: authentication-ip.cfg -# * X.509 Certificate Authentication -# Plugin class: org.dspace.authenticate.X509Authentication -# Configuration file: authentication-x509.cfg # * ORCID certificate authentication. # Plugin class: org.dspace.authenticate.OrcidAuthentication # Configuration file: orcid.cfg @@ -49,9 +46,6 @@ # Shibboleth authentication/authorization. See authentication-shibboleth.cfg for default configuration. #plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.ShibAuthentication -# X.509 certificate authentication. See authentication-x509.cfg for default configuration. -#plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.X509Authentication - # ORCID certificate authentication. # plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.OrcidAuthentication @@ -84,8 +78,9 @@ jwt.login.encryption.enabled = false # of some performance, this setting WILL ONLY BE used when encrypting the jwt. jwt.login.compression.enabled = true -# Expiration time of a token in milliseconds -jwt.login.token.expiration = 1800000 +# Expiration time of a login token in milliseconds +# Default: 1800000 (30 minutes) +#jwt.login.token.expiration = 1800000 #---------------------------------------------------------------# #---Stateless JWT Authentication for downloads of bitstreams----# @@ -109,5 +104,6 @@ jwt.shortLived.encryption.enabled = false # of some performance, this setting WILL ONLY BE used when encrypting the jwt. jwt.shortLived.compression.enabled = true -# Expiration time of a token in milliseconds -jwt.shortLived.token.expiration = 2000 +# Expiration time of a short-lived token in milliseconds +# Default: 2000 (2 seconds) +#jwt.shortLived.token.expiration = 2000 diff --git a/dspace/config/modules/bulkedit.cfg b/dspace/config/modules/bulkedit.cfg index e326e007f884..7a9685d09ace 100644 --- a/dspace/config/modules/bulkedit.cfg +++ b/dspace/config/modules/bulkedit.cfg @@ -14,10 +14,9 @@ # The delimiter used to serarate authority data (defaults to a double colon ::) # bulkedit.authorityseparator = :: -# A hard limit of the number of items allowed to be edited in one go in the UI -# (does not apply to the command line version) -# TODO: UNSUPPORTED in DSpace 7.0 -# bulkedit.gui-item-limit = 20 +# A hard limit on the number of items allowed to be imported via the UI. +# To disable this limit, set this value to 0. Defaults to 1000. +bulkedit.import.max.items = 1000 # Metadata elements to exclude when exporting via the user interfaces, or when using the # command line version and not using the -a (all) option. @@ -45,3 +44,6 @@ bulkedit.change.commit.count = 100 # Recommend to keep this at a feasible number, as exporting large amounts of items can be resource intensive # If not set, this will default to 500 items # bulkedit.export.max.items = 500 + +# Bulkedit setting to add metadata.hide fields to ignored fields defaults to true +# bulkedit.ignore-on-export.include-metadata-hide = true diff --git a/dspace/config/modules/curate.cfg b/dspace/config/modules/curate.cfg index 6e75738de543..dad40d615454 100644 --- a/dspace/config/modules/curate.cfg +++ b/dspace/config/modules/curate.cfg @@ -29,3 +29,15 @@ curate.taskqueue.dir = ${dspace.dir}/ctqueues # Maximum amount of redirects set to 0 for none and -1 for unlimited curate.checklinks.max-redirect = 0 + +# allowed base path(s) of curation task files +# it is recommended to restrict this path as much as possible +# so that the DSpace Processes framework may only load files as "tasks" +# from a trusted location. For multiple paths, repeat this configuration +# property for each trusted path +# Default: ${dspace.dir} +#curate.taskfile.base = ${dspace.dir} + +# allowed base path of reporter output. +# Default: ${dspace.dir}/log +#curate.reporter.base = ${dspace.dir}/log diff --git a/dspace/config/modules/discovery.cfg b/dspace/config/modules/discovery.cfg index cd8e8636c2e3..6b3e8316d21d 100644 --- a/dspace/config/modules/discovery.cfg +++ b/dspace/config/modules/discovery.cfg @@ -54,3 +54,9 @@ discovery.facet.namedtype.workflow.pooled = 004workflow\n|||\nWaiting for Contro # Set to -1 if stale objects should be ignored. Set to 0 if you want to avoid extra query but take the chance to cleanup # the index each time that stale objects are found. Default 3 discovery.removestale.attempts = 3 + +# Set to true to escape HTML tags in hit highlight results +discovery.highlights.escape-html = true +# Set the fields that should not escape HTML tags in hit highlight results when discovery.highlights.escape-html is true +# It is possible to provide multiple fields by separating them by commas like this: dc.description.abstract, dc.title +# discovery.highlights.html-allowed-fields = diff --git a/dspace/config/modules/external-providers.cfg b/dspace/config/modules/external-providers.cfg index f210a0aa5163..cbb2b4ea0cbf 100644 --- a/dspace/config/modules/external-providers.cfg +++ b/dspace/config/modules/external-providers.cfg @@ -45,6 +45,9 @@ epo.searchUrl = https://ops.epo.org/rest-services/published-data/search ################################################################# #---------------------- PubMed -----------------------------# #---------------------------------------------------------------# +# If apiKey is set then it's used, if not set or blank then it's not +# Max amount of requests per ip per second with apiKey is 10; without 3 +pubmed.apiKey = pubmed.url.search = https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi pubmed.url.fetch = https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi ################################################################# @@ -95,5 +98,8 @@ datacite.timeout = 180000 #--------------------------- ROR -------------------------------# #---------------------------------------------------------------# -ror.orgunit-import.api-url = https://api.ror.org/organizations -################################################################# \ No newline at end of file +ror.orgunit-import.api-url = https://api.ror.org/v2/organizations +# This client-id must be set inside local.cfg you can find details on how to generate it here: +# https://ror.readme.io/docs/client-id#how-to-register-a-client-id +# ror.client-id = +################################################################# diff --git a/dspace/config/modules/ldn.cfg b/dspace/config/modules/ldn.cfg index 96d4e39ffc11..8219070cccf3 100644 --- a/dspace/config/modules/ldn.cfg +++ b/dspace/config/modules/ldn.cfg @@ -40,6 +40,11 @@ ldn.notify.inbox.block-untrusted-ip = true # this is the medatada used to retrieve the relation with external items when sending relationship requests #ldn.notify.relation.metadata = dc.relation +# Base path of LDN templates. Apache Velocity will only +# load templates that begin with this base path, and relative paths will use this as a base when +# calculating the absolute path. This is not repeatable, multiple paths will be ignored beyond the first. +# Default is ${dspace.dir}/config/ldn +#ldn.template.path = ${dspace.dir}/config/ldn # EMAIL CONFIGURATION # Supported values for actionSendFilter are: diff --git a/dspace/config/modules/oai.cfg b/dspace/config/modules/oai.cfg index 98b10f59dee9..1d36cce175d2 100644 --- a/dspace/config/modules/oai.cfg +++ b/dspace/config/modules/oai.cfg @@ -16,6 +16,14 @@ oai.enabled = true # (Requires reboot of servlet container, e.g. Tomcat, to reload) oai.path = oai +# Whether or not to enable the OAI HTML interface, useful for +# debugging and learning about how OAI-PMH works +# For the HTML interface to work, the stylesheet must also be +# configured in config/crosswalks/oai/xoai.xml +# (by default dspace-oai/src/main/resources/static/style.xsl) +# Support for the OAI HTML interface is turned on by default. +#oai.html = true + # Storage: solr | database (solr is recommended) oai.storage=solr @@ -30,9 +38,14 @@ oai.solr.url=${solr.server}/${solr.multicorePrefix}oai # This field is used for two purposes: # 1. As your OAI-PMH # 2. As the prefix for all Identifiers in OAI-PMH (Format is "oai:${oai.identifier.prefix}:${handle.prefix}") -# The OAI-PMH spec requires this prefix to correspond to your site's hostname. Therefore, by default, -# DSpace will set this configuration to the hostname from your ${dspace.ui.url} configuration. -# However, you may override that default value by uncommenting this configuration. +# The OAI-PMH spec requires this prefix to correspond to your site's hostname (without protocol). +# By default, DSpace automatically extracts the hostname from your ${dspace.ui.url} setting +# (e.g. "https://demo.dspace.org" becomes "demo.dspace.org"). +# +# WARNING: Only uncomment this if you need a DIFFERENT hostname than what ${dspace.ui.url} provides. +# The value must be a plain hostname (e.g. "demo.dspace.org"), NOT a full URL. +# Do NOT set this to ${dspace.ui.url} -- that would include the protocol (https://) in OAI identifiers, +# which violates the OAI-PMH specification. # oai.identifier.prefix = YOUR_SITE_HOSTNAME # Base url for bitstreams @@ -132,3 +145,10 @@ oai.harvester.unknownSchema = fail # when attempting to find the handle of harvested items. If there is a match with # this config parameter, a new handle will be minted instead. Default value: 123456789. #oai.harvester.rejectedHandlePrefix = 123456789, myTestHandle + +# If ingesting files with ORE, only files with URLs that match the base URL of the remote +# OAI endpoint's domain name are accepted, or a list of other URL prefixes defined below +#oai.harvester.ore.file.validateUrlPrefix = true +# Prefixes that are allowed globally (for any endpoint) are below +#oai.harvester.ore.file.allowedUrlPrefix = dspace.myinstitution.edu +#oai.harvester.ore.file.allowedUrlPrefix = files.myinstitution.edu diff --git a/dspace/config/modules/orcid.cfg b/dspace/config/modules/orcid.cfg index ad31371cb890..e3c021f9c631 100644 --- a/dspace/config/modules/orcid.cfg +++ b/dspace/config/modules/orcid.cfg @@ -1,4 +1,3 @@ - #------------------------------------------------------------------# #--------------------ORCID GENERIC CONFIGURATIONS------------------# #------------------------------------------------------------------# @@ -61,12 +60,18 @@ orcid.mapping.work.contributors = dc.contributor.editor::editor ##orcid.mapping.work.external-ids syntax is :: or $simple-handle:: ##The full list of available external identifiers is available here https://pub.orcid.org/v3.0/identifiers +# The identifiers need to have a relationship of SELF, PART_OF, VERSION_OF or FUNDED_BY. +# The default for most identifiers is SELF. The default for identifiers more commonly +# associated with 'parent' publciations (ISSN, ISBN) is PART_OF. +# See the map in `orcid-services.xml` +# VERSION_OF and FUNDED_BY are not currently implemented. orcid.mapping.work.external-ids = dc.identifier.doi::doi orcid.mapping.work.external-ids = dc.identifier.scopus::eid orcid.mapping.work.external-ids = dc.identifier.pmid::pmid orcid.mapping.work.external-ids = $simple-handle::handle orcid.mapping.work.external-ids = dc.identifier.isi::wosuid orcid.mapping.work.external-ids = dc.identifier.issn::issn +orcid.mapping.work.external-ids = dc.identifier.isbn::isbn ### Funding mapping ### orcid.mapping.funding.title = dc.title @@ -146,6 +151,9 @@ orcid.bulk-synchronization.max-attempts = 5 #--------------------ORCID EXTERNAL DATA MAPPING-------------------# #------------------------------------------------------------------# +# Note - the below mapping is for ORCID->DSpace imports, not for +# DSpace->ORCID exports (see orcid.mapping.work.*) + ### Work (Publication) external-data.mapping ### orcid.external-data.mapping.publication.title = dc.title diff --git a/dspace/config/modules/rest.cfg b/dspace/config/modules/rest.cfg index 3b1d941422f0..4e8fd16a142d 100644 --- a/dspace/config/modules/rest.cfg +++ b/dspace/config/modules/rest.cfg @@ -71,3 +71,4 @@ rest.properties.exposed = dspace.name rest.properties.exposed = dspace.baseUrl # End UMD Customization rest.properties.exposed = bulkedit.export.max.items +rest.properties.exposed = orcid.domain-url diff --git a/dspace/config/modules/signposting.cfg b/dspace/config/modules/signposting.cfg index 6acfa74bdd07..eb2aa81d36b0 100644 --- a/dspace/config/modules/signposting.cfg +++ b/dspace/config/modules/signposting.cfg @@ -34,5 +34,10 @@ signposting.describedby.crosswalk-name = DataCite # Mime-type of response of handling of 'describedby' links. signposting.describedby.mime-type = application/vnd.datacite.datacite+xml +# Limit to the number of an item's bitstreams to return as typed links. +# If there are more bitstreams than this limit then only the typed links to the Link Sets are added to the header. +# Defaults to 10 if the value is unspecified +# signposting.item.bitstreams.limit = 10 + # Optional, to expose the profile attribute, required by PCI workflow () -# signposting.describedby.profile = http://datacite.org/schema/kernel-4 \ No newline at end of file +# signposting.describedby.profile = http://datacite.org/schema/kernel-4 diff --git a/dspace/config/modules/usage-statistics.cfg b/dspace/config/modules/usage-statistics.cfg index c77bb1ca78a3..6d47a13dcfc4 100644 --- a/dspace/config/modules/usage-statistics.cfg +++ b/dspace/config/modules/usage-statistics.cfg @@ -60,4 +60,21 @@ usage-statistics.shardedByYear = false #anonymize_statistics.dns_mask = anonymized # Only anonymize statistics records older than this threshold (expressed in days) -#anonymize_statistics.time_threshold = 90 \ No newline at end of file +#anonymize_statistics.time_threshold = 90 + +# Maximum number of items to display in the usage statistics report for an entire repository +usage-statistics.topItemsLimit = 10 + +# Number of months to begin retrieving usage statistics for total visits per month of a DSpace object +# For example, -6 means include the previous six months +usage-statistics.startDateInterval = -6 + +# Number of months to end retrieving usage statistics for total visits per month of a DSpace object +# For example, +1 means include the current month +usage-statistics.endDateInterval = +1 + +# Maximum number of countries to display in the usage statistics reports +usage-statistics.topCountriesLimit = 100 + +# Maximum number of cities to display in the usage statistics reports +usage-statistics.topCitiesLimit = 100 diff --git a/dspace/config/registries/journal-types.xml b/dspace/config/registries/journal-types.xml new file mode 100644 index 000000000000..c68bee1ce6c1 --- /dev/null +++ b/dspace/config/registries/journal-types.xml @@ -0,0 +1,37 @@ + + + + + + DSpace Journal Types + + + + + + journal + http://dspace.org/journal + + + + journal + title + + The title of the Journal related to this object + + + + + journalvolume + http://dspace.org/journalvolume + + + + journalvolume + identifier + name + The identifier name for the Journal Volume related to this object + + + diff --git a/dspace/config/registries/openaire4-types.xml b/dspace/config/registries/openaire4-types.xml index e47e06e0aebf..17dccb222dc8 100644 --- a/dspace/config/registries/openaire4-types.xml +++ b/dspace/config/registries/openaire4-types.xml @@ -2,13 +2,13 @@ - Openaire4 fields definition + OpenAIRE v4 fields definition @@ -102,4 +102,4 @@ The date when the conference took place. This property is considered to be part of the bibliographic citation. Recommended best practice for encoding the date value is defined in a profile of ISO 8601 [W3CDTF] and follows the YYYY-MM-DD format. - + \ No newline at end of file diff --git a/dspace/config/registries/schema-person-types.xml b/dspace/config/registries/schema-person-types.xml index 0a40060e5101..55f1acbbb74d 100644 --- a/dspace/config/registries/schema-person-types.xml +++ b/dspace/config/registries/schema-person-types.xml @@ -156,4 +156,13 @@ Full name variant + + + person + contributor + other + + + \ No newline at end of file diff --git a/dspace/config/spring/api/bitstore.xml b/dspace/config/spring/api/bitstore.xml index ab7dedaf4f85..b874fc88d71b 100644 --- a/dspace/config/spring/api/bitstore.xml +++ b/dspace/config/spring/api/bitstore.xml @@ -51,6 +51,9 @@ + + + @@ -58,6 +61,32 @@ + + + + + + + + + + + + + + + + + + diff --git a/dspace/config/spring/api/core-services.xml b/dspace/config/spring/api/core-services.xml index 041dcf37b4a2..d5597ea7c6ce 100644 --- a/dspace/config/spring/api/core-services.xml +++ b/dspace/config/spring/api/core-services.xml @@ -32,7 +32,7 @@ - + diff --git a/dspace/config/spring/api/discovery.xml b/dspace/config/spring/api/discovery.xml index b93b3c2cd27e..cd1a84993007 100644 --- a/dspace/config/spring/api/discovery.xml +++ b/dspace/config/spring/api/discovery.xml @@ -30,8 +30,6 @@ - - diff --git a/dspace/config/spring/api/orcid-services.xml b/dspace/config/spring/api/orcid-services.xml index 6ec9be9fdf5d..c7a131832de1 100644 --- a/dspace/config/spring/api/orcid-services.xml +++ b/dspace/config/spring/api/orcid-services.xml @@ -55,24 +55,45 @@ - - - - - - - - - - - - - - - - - - + + + + + journal-article + magazine-article + newspaper-article + data-set + learning-object + other + + + + + book-chapter + book-review + other + + + + + + + + + + + + + + + + + + + + + + diff --git a/dspace/config/spring/api/ror-integration.xml b/dspace/config/spring/api/ror-integration.xml index ff554612052e..f205817e8c7b 100644 --- a/dspace/config/spring/api/ror-integration.xml +++ b/dspace/config/spring/api/ror-integration.xml @@ -18,20 +18,29 @@ + + - + + + + + + + + @@ -40,22 +49,37 @@ + - + + + + + + + + - + + + + + + + + @@ -64,14 +88,33 @@ + - + + + + + + + + + + + + + + + + + + + @@ -80,22 +123,37 @@ + - + + + + + + + + - + + + + + + + + @@ -103,7 +161,6 @@ - diff --git a/dspace/config/spring/api/virtual-metadata.xml b/dspace/config/spring/api/virtual-metadata.xml index dee96eca7c0c..c6d9a0a622f2 100644 --- a/dspace/config/spring/api/virtual-metadata.xml +++ b/dspace/config/spring/api/virtual-metadata.xml @@ -40,6 +40,7 @@ This value-ref should be a bean of type VirtualMetadataConfiguration --> + @@ -64,21 +62,14 @@ - - - - - - - + - - + diff --git a/dspace/config/submission-forms.xml b/dspace/config/submission-forms.xml index d426f99b32b2..77d57388ef9b 100644 --- a/dspace/config/submission-forms.xml +++ b/dspace/config/submission-forms.xml @@ -39,7 +39,7 @@ dc description - true + false textarea Enter a description for the file @@ -94,7 +94,7 @@ issued false - + date Please give the date of previous publication or public distribution. You can leave out the day and/or month if they aren't applicable. @@ -108,7 +108,7 @@ false - + onebox Enter the name of the publisher of the previously issued instance of this item. @@ -296,7 +296,7 @@ issued false - + date Please give the date of previous publication or public distribution. You can leave out the day and/or month if they aren't applicable. @@ -310,7 +310,7 @@ false - + onebox Enter the name of the publisher of the previously issued instance of this item. @@ -874,7 +874,7 @@ issued false - + date Please give the date of previous publication or public distribution. You can leave out the day @@ -890,7 +890,7 @@ false - + onebox Enter the name of the publisher of the previously issued instance of this item. @@ -2320,8 +2320,8 @@ - diff --git a/dspace/docs/DockerDevelopmentEnvironment.md b/dspace/docs/DockerDevelopmentEnvironment.md index 6a3a280c237c..f0285840e9e8 100644 --- a/dspace/docs/DockerDevelopmentEnvironment.md +++ b/dspace/docs/DockerDevelopmentEnvironment.md @@ -306,8 +306,7 @@ RUN apt-get update && \ libconfig-properties-perl \ jq \ && apt-get purge -y --auto-remove \ - && rm -rf /var/lib/apt/lists/* \ - && mkfifo /var/spool/postfix/public/pickup + && rm -rf /var/lib/apt/lists/* # End Dependencies for email functionality ``` diff --git a/dspace/modules/additions/pom.xml b/dspace/modules/additions/pom.xml index bb34ce97ab2e..ad81e31724ff 100644 --- a/dspace/modules/additions/pom.xml +++ b/dspace/modules/additions/pom.xml @@ -17,7 +17,7 @@ org.dspace modules - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT .. @@ -359,5 +359,4 @@ - diff --git a/dspace/modules/additions/src/main/resources/Messages.properties b/dspace/modules/additions/src/main/resources/Messages.properties index f444c3a29872..0f3ee9a90743 100644 --- a/dspace/modules/additions/src/main/resources/Messages.properties +++ b/dspace/modules/additions/src/main/resources/Messages.properties @@ -72,20 +72,20 @@ org.dspace.checker.ResultsLogger.store-number org.dspace.checker.ResultsLogger.to-be-processed = To be processed org.dspace.checker.ResultsLogger.user-format-description = User format description org.dspace.checker.SimpleReporterImpl.bitstream-id = Bitstream Id -org.dspace.checker.SimpleReporterImpl.bitstream-not-found-report = The following is a BITSTREAM NOT FOUND report for -org.dspace.checker.SimpleReporterImpl.bitstream-will-no-longer-be-processed = The following is a BITSTREAM WILL NO LONGER BE PROCESSED report for +org.dspace.checker.SimpleReporterImpl.bitstream-not-found-report = The following is a BITSTREAM NOT FOUND report from +org.dspace.checker.SimpleReporterImpl.bitstream-will-no-longer-be-processed = The following is a BITSTREAM WILL NO LONGER BE PROCESSED report from org.dspace.checker.SimpleReporterImpl.check-id = Check Id org.dspace.checker.SimpleReporterImpl.checksum = Checksum org.dspace.checker.SimpleReporterImpl.checksum-algorithm = Checksum Algorithm org.dspace.checker.SimpleReporterImpl.checksum-calculated = Checksum Calculated -org.dspace.checker.SimpleReporterImpl.checksum-did-not-match = The following is a CHECKSUM DID NOT MATCH report for +org.dspace.checker.SimpleReporterImpl.checksum-did-not-match = The following is a CHECKSUM DID NOT MATCH report from org.dspace.checker.SimpleReporterImpl.checksum-expected = Checksum Expected org.dspace.checker.SimpleReporterImpl.date-range-to = to org.dspace.checker.SimpleReporterImpl.deleted = Deleted -org.dspace.checker.SimpleReporterImpl.deleted-bitstream-intro = The following is a BITSTREAM SET DELETED report for +org.dspace.checker.SimpleReporterImpl.deleted-bitstream-intro = The following is a BITSTREAM SET DELETED report from org.dspace.checker.SimpleReporterImpl.description = Description org.dspace.checker.SimpleReporterImpl.format-id = Format Id -org.dspace.checker.SimpleReporterImpl.howto-add-unchecked-bitstreams = To add these bitstreams to be checked run the checksum checker with the -u option +org.dspace.checker.SimpleReporterImpl.howto-add-unchecked-bitstreams = To add these bitstreams to be checked run the checksum checker again org.dspace.checker.SimpleReporterImpl.internal-id = Internal Id org.dspace.checker.SimpleReporterImpl.name = Name org.dspace.checker.SimpleReporterImpl.no-bitstreams-changed = There were no bitstreams found with changed checksums diff --git a/dspace/modules/additions/src/main/resources/mime.types b/dspace/modules/additions/src/main/resources/mime.types new file mode 100644 index 000000000000..6707e7e794f4 --- /dev/null +++ b/dspace/modules/additions/src/main/resources/mime.types @@ -0,0 +1,1863 @@ +# The mime.types file comes from the iiif-apis library https://github.com/dbmdz/iiif-apis/blob/main/src/main/resources/mime.types. +# The mime.types file is protected by the MIT license defined here https://github.com/dbmdz/iiif-apis/blob/main/LICENSE. + +# Due to this issue https://github.com/dbmdz/iiif-apis/issues/270 +# The mime.types file has been added to the DSpace sources. +# The mime.types file can be removed as soon as the ticket is closed. + + +# This file maps Internet media types to unique file extension(s). +# Although created for httpd, this file is used by many software systems +# and has been placed in the public domain for unlimited redisribution. +# +# The table below contains both registered and (common) unregistered types. +# A type that has no unique extension can be ignored -- they are listed +# here to guide configurations toward known types and to make it easier to +# identify "new" types. File extensions are also commonly used to indicate +# content languages and encodings, so choose them carefully. +# +# Internet media types should be registered as described in RFC 4288. +# The registry is at . +# +# MIME type (lowercased) Extensions +# ============================================ ========== +# application/1d-interleaved-parityfec +# application/3gpdash-qoe-report+xml +# application/3gpp-ims+xml +# application/a2l +# application/activemessage +# application/alto-costmap+json +# application/alto-costmapfilter+json +# application/alto-directory+json +# application/alto-endpointcost+json +# application/alto-endpointcostparams+json +# application/alto-endpointprop+json +# application/alto-endpointpropparams+json +# application/alto-error+json +# application/alto-networkmap+json +# application/alto-networkmapfilter+json +# application/aml +application/andrew-inset ez +# application/applefile +application/applixware aw +# application/atf +# application/atfx +application/atom+xml atom +application/atomcat+xml atomcat +# application/atomdeleted+xml +# application/atomicmail +application/atomsvc+xml atomsvc +# application/atxml +# application/auth-policy+xml +# application/bacnet-xdd+zip +# application/batch-smtp +# application/beep+xml +# application/calendar+json +# application/calendar+xml +# application/call-completion +# application/cals-1840 +# application/cbor +# application/ccmp+xml +application/ccxml+xml ccxml +# application/cdfx+xml +application/cdmi-capability cdmia +application/cdmi-container cdmic +application/cdmi-domain cdmid +application/cdmi-object cdmio +application/cdmi-queue cdmiq +# application/cdni +# application/cea +# application/cea-2018+xml +# application/cellml+xml +# application/cfw +# application/cms +# application/cnrp+xml +# application/coap-group+json +# application/commonground +# application/conference-info+xml +# application/cpl+xml +# application/csrattrs +# application/csta+xml +# application/cstadata+xml +# application/csvm+json +application/cu-seeme cu +# application/cybercash +# application/dash+xml +# application/dashdelta +application/davmount+xml davmount +# application/dca-rft +# application/dcd +# application/dec-dx +# application/dialog-info+xml +# application/dicom +# application/dii +# application/dit +# application/dns +application/docbook+xml dbk +# application/dskpp+xml +application/dssc+der dssc +application/dssc+xml xdssc +# application/dvcs +application/ecmascript ecma +# application/edi-consent +# application/edi-x12 +# application/edifact +# application/efi +# application/emergencycalldata.comment+xml +# application/emergencycalldata.deviceinfo+xml +# application/emergencycalldata.providerinfo+xml +# application/emergencycalldata.serviceinfo+xml +# application/emergencycalldata.subscriberinfo+xml +application/emma+xml emma +# application/emotionml+xml +# application/encaprtp +# application/epp+xml +application/epub+zip epub +# application/eshop +# application/example +application/exi exi +# application/fastinfoset +# application/fastsoap +# application/fdt+xml +# application/fits +application/font-tdpfr pfr +# application/framework-attributes+xml +# application/geo+json +application/gml+xml gml +application/gpx+xml gpx +application/gxf gxf +# application/gzip +# application/h224 +# application/held+xml +# application/http +application/hyperstudio stk +# application/ibe-key-request+xml +# application/ibe-pkg-reply+xml +# application/ibe-pp-data +# application/iges +# application/im-iscomposing+xml +# application/index +# application/index.cmd +# application/index.obj +# application/index.response +# application/index.vnd +application/inkml+xml ink inkml +# application/iotp +application/ipfix ipfix +# application/ipp +# application/isup +# application/its+xml +application/java-archive jar +application/java-serialized-object ser +application/java-vm class +application/javascript js +# application/jose +# application/jose+json +# application/jrd+json +application/json json +# application/json-patch+json +# application/json-seq +application/jsonml+json jsonml +# application/jwk+json +# application/jwk-set+json +# application/jwt +# application/kpml-request+xml +# application/kpml-response+xml +# application/ld+json +# application/lgr+xml +# application/link-format +# application/load-control+xml +application/lost+xml lostxml +# application/lostsync+xml +# application/lxf +application/mac-binhex40 hqx +application/mac-compactpro cpt +# application/macwriteii +application/mads+xml mads +application/marc mrc +application/marcxml+xml mrcx +application/mathematica ma nb mb +application/mathml+xml mathml +# application/mathml-content+xml +# application/mathml-presentation+xml +# application/mbms-associated-procedure-description+xml +# application/mbms-deregister+xml +# application/mbms-envelope+xml +# application/mbms-msk+xml +# application/mbms-msk-response+xml +# application/mbms-protection-description+xml +# application/mbms-reception-report+xml +# application/mbms-register+xml +# application/mbms-register-response+xml +# application/mbms-schedule+xml +# application/mbms-user-service-description+xml +application/mbox mbox +# application/media-policy-dataset+xml +# application/media_control+xml +application/mediaservercontrol+xml mscml +# application/merge-patch+json +application/metalink+xml metalink +application/metalink4+xml meta4 +application/mets+xml mets +# application/mf4 +# application/mikey +application/mods+xml mods +# application/moss-keys +# application/moss-signature +# application/mosskey-data +# application/mosskey-request +application/mp21 m21 mp21 +application/mp4 mp4s +# application/mpeg4-generic +# application/mpeg4-iod +# application/mpeg4-iod-xmt +# application/mrb-consumer+xml +# application/mrb-publish+xml +# application/msc-ivr+xml +# application/msc-mixer+xml +application/msword doc dot +application/mxf mxf +# application/nasdata +# application/news-checkgroups +# application/news-groupinfo +# application/news-transmission +# application/nlsml+xml +# application/nss +# application/ocsp-request +# application/ocsp-response +application/octet-stream bin dms lrf mar so dist distz pkg bpk dump elc deploy +application/oda oda +# application/odx +application/oebps-package+xml opf +application/ogg ogx +application/omdoc+xml omdoc +application/onenote onetoc onetoc2 onetmp onepkg +application/oxps oxps +# application/p2p-overlay+xml +# application/parityfec +application/patch-ops-error+xml xer +application/pdf pdf +# application/pdx +application/pgp-encrypted pgp +# application/pgp-keys +application/pgp-signature asc sig +application/pics-rules prf +# application/pidf+xml +# application/pidf-diff+xml +application/pkcs10 p10 +# application/pkcs12 +application/pkcs7-mime p7m p7c +application/pkcs7-signature p7s +application/pkcs8 p8 +application/pkix-attr-cert ac +application/pkix-cert cer +application/pkix-crl crl +application/pkix-pkipath pkipath +application/pkixcmp pki +application/pls+xml pls +# application/poc-settings+xml +application/postscript ai eps ps +# application/ppsp-tracker+json +# application/problem+json +# application/problem+xml +# application/provenance+xml +# application/prs.alvestrand.titrax-sheet +application/prs.cww cww +# application/prs.hpub+zip +# application/prs.nprend +# application/prs.plucker +# application/prs.rdf-xml-crypt +# application/prs.xsf+xml +application/pskc+xml pskcxml +# application/qsig +# application/raptorfec +# application/rdap+json +application/rdf+xml rdf +application/reginfo+xml rif +application/relax-ng-compact-syntax rnc +# application/remote-printing +# application/reputon+json +application/resource-lists+xml rl +application/resource-lists-diff+xml rld +# application/rfc+xml +# application/riscos +# application/rlmi+xml +application/rls-services+xml rs +application/rpki-ghostbusters gbr +application/rpki-manifest mft +application/rpki-roa roa +# application/rpki-updown +application/rsd+xml rsd +application/rss+xml rss +application/rtf rtf +# application/rtploopback +# application/rtx +# application/samlassertion+xml +# application/samlmetadata+xml +application/sbml+xml sbml +# application/scaip+xml +# application/scim+json +application/scvp-cv-request scq +application/scvp-cv-response scs +application/scvp-vp-request spq +application/scvp-vp-response spp +application/sdp sdp +# application/sep+xml +# application/sep-exi +# application/session-info +# application/set-payment +application/set-payment-initiation setpay +# application/set-registration +application/set-registration-initiation setreg +# application/sgml +# application/sgml-open-catalog +application/shf+xml shf +# application/sieve +# application/simple-filter+xml +# application/simple-message-summary +# application/simplesymbolcontainer +# application/slate +# application/smil +application/smil+xml smi smil +# application/smpte336m +# application/soap+fastinfoset +# application/soap+xml +application/sparql-query rq +application/sparql-results+xml srx +# application/spirits-event+xml +# application/sql +application/srgs gram +application/srgs+xml grxml +application/sru+xml sru +application/ssdl+xml ssdl +application/ssml+xml ssml +# application/tamp-apex-update +# application/tamp-apex-update-confirm +# application/tamp-community-update +# application/tamp-community-update-confirm +# application/tamp-error +# application/tamp-sequence-adjust +# application/tamp-sequence-adjust-confirm +# application/tamp-status-query +# application/tamp-status-response +# application/tamp-update +# application/tamp-update-confirm +application/tei+xml tei teicorpus +application/thraud+xml tfi +# application/timestamp-query +# application/timestamp-reply +application/timestamped-data tsd +# application/ttml+xml +# application/tve-trigger +# application/ulpfec +# application/urc-grpsheet+xml +# application/urc-ressheet+xml +# application/urc-targetdesc+xml +# application/urc-uisocketdesc+xml +# application/vcard+json +# application/vcard+xml +# application/vemmi +# application/vividence.scriptfile +# application/vnd.3gpp-prose+xml +# application/vnd.3gpp-prose-pc3ch+xml +# application/vnd.3gpp.access-transfer-events+xml +# application/vnd.3gpp.bsf+xml +# application/vnd.3gpp.mid-call+xml +application/vnd.3gpp.pic-bw-large plb +application/vnd.3gpp.pic-bw-small psb +application/vnd.3gpp.pic-bw-var pvb +# application/vnd.3gpp.sms +# application/vnd.3gpp.sms+xml +# application/vnd.3gpp.srvcc-ext+xml +# application/vnd.3gpp.srvcc-info+xml +# application/vnd.3gpp.state-and-event-info+xml +# application/vnd.3gpp.ussd+xml +# application/vnd.3gpp2.bcmcsinfo+xml +# application/vnd.3gpp2.sms +application/vnd.3gpp2.tcap tcap +# application/vnd.3lightssoftware.imagescal +application/vnd.3m.post-it-notes pwn +application/vnd.accpac.simply.aso aso +application/vnd.accpac.simply.imp imp +application/vnd.acucobol acu +application/vnd.acucorp atc acutc +application/vnd.adobe.air-application-installer-package+zip air +# application/vnd.adobe.flash.movie +application/vnd.adobe.formscentral.fcdt fcdt +application/vnd.adobe.fxp fxp fxpl +# application/vnd.adobe.partial-upload +application/vnd.adobe.xdp+xml xdp +application/vnd.adobe.xfdf xfdf +# application/vnd.aether.imp +# application/vnd.ah-barcode +application/vnd.ahead.space ahead +application/vnd.airzip.filesecure.azf azf +application/vnd.airzip.filesecure.azs azs +application/vnd.amazon.ebook azw +# application/vnd.amazon.mobi8-ebook +application/vnd.americandynamics.acc acc +application/vnd.amiga.ami ami +# application/vnd.amundsen.maze+xml +application/vnd.android.package-archive apk +# application/vnd.anki +application/vnd.anser-web-certificate-issue-initiation cii +application/vnd.anser-web-funds-transfer-initiation fti +application/vnd.antix.game-component atx +# application/vnd.apache.thrift.binary +# application/vnd.apache.thrift.compact +# application/vnd.apache.thrift.json +# application/vnd.api+json +application/vnd.apple.installer+xml mpkg +application/vnd.apple.mpegurl m3u8 +# application/vnd.arastra.swi +application/vnd.aristanetworks.swi swi +# application/vnd.artsquare +application/vnd.astraea-software.iota iota +application/vnd.audiograph aep +# application/vnd.autopackage +# application/vnd.avistar+xml +# application/vnd.balsamiq.bmml+xml +# application/vnd.balsamiq.bmpr +# application/vnd.bekitzur-stech+json +# application/vnd.biopax.rdf+xml +application/vnd.blueice.multipass mpm +# application/vnd.bluetooth.ep.oob +# application/vnd.bluetooth.le.oob +application/vnd.bmi bmi +application/vnd.businessobjects rep +# application/vnd.cab-jscript +# application/vnd.canon-cpdl +# application/vnd.canon-lips +# application/vnd.cendio.thinlinc.clientconf +# application/vnd.century-systems.tcp_stream +application/vnd.chemdraw+xml cdxml +# application/vnd.chess-pgn +application/vnd.chipnuts.karaoke-mmd mmd +application/vnd.cinderella cdy +# application/vnd.cirpack.isdn-ext +# application/vnd.citationstyles.style+xml +application/vnd.claymore cla +application/vnd.cloanto.rp9 rp9 +application/vnd.clonk.c4group c4g c4d c4f c4p c4u +application/vnd.cluetrust.cartomobile-config c11amc +application/vnd.cluetrust.cartomobile-config-pkg c11amz +# application/vnd.coffeescript +# application/vnd.collection+json +# application/vnd.collection.doc+json +# application/vnd.collection.next+json +# application/vnd.comicbook+zip +# application/vnd.commerce-battelle +application/vnd.commonspace csp +application/vnd.contact.cmsg cdbcmsg +# application/vnd.coreos.ignition+json +application/vnd.cosmocaller cmc +application/vnd.crick.clicker clkx +application/vnd.crick.clicker.keyboard clkk +application/vnd.crick.clicker.palette clkp +application/vnd.crick.clicker.template clkt +application/vnd.crick.clicker.wordbank clkw +application/vnd.criticaltools.wbs+xml wbs +application/vnd.ctc-posml pml +# application/vnd.ctct.ws+xml +# application/vnd.cups-pdf +# application/vnd.cups-postscript +application/vnd.cups-ppd ppd +# application/vnd.cups-raster +# application/vnd.cups-raw +# application/vnd.curl +application/vnd.curl.car car +application/vnd.curl.pcurl pcurl +# application/vnd.cyan.dean.root+xml +# application/vnd.cybank +application/vnd.dart dart +application/vnd.data-vision.rdz rdz +# application/vnd.debian.binary-package +application/vnd.dece.data uvf uvvf uvd uvvd +application/vnd.dece.ttml+xml uvt uvvt +application/vnd.dece.unspecified uvx uvvx +application/vnd.dece.zip uvz uvvz +application/vnd.denovo.fcselayout-link fe_launch +# application/vnd.desmume.movie +# application/vnd.dir-bi.plate-dl-nosuffix +# application/vnd.dm.delegation+xml +application/vnd.dna dna +# application/vnd.document+json +application/vnd.dolby.mlp mlp +# application/vnd.dolby.mobile.1 +# application/vnd.dolby.mobile.2 +# application/vnd.doremir.scorecloud-binary-document +application/vnd.dpgraph dpg +application/vnd.dreamfactory dfac +# application/vnd.drive+json +application/vnd.ds-keypoint kpxx +# application/vnd.dtg.local +# application/vnd.dtg.local.flash +# application/vnd.dtg.local.html +application/vnd.dvb.ait ait +# application/vnd.dvb.dvbj +# application/vnd.dvb.esgcontainer +# application/vnd.dvb.ipdcdftnotifaccess +# application/vnd.dvb.ipdcesgaccess +# application/vnd.dvb.ipdcesgaccess2 +# application/vnd.dvb.ipdcesgpdd +# application/vnd.dvb.ipdcroaming +# application/vnd.dvb.iptv.alfec-base +# application/vnd.dvb.iptv.alfec-enhancement +# application/vnd.dvb.notif-aggregate-root+xml +# application/vnd.dvb.notif-container+xml +# application/vnd.dvb.notif-generic+xml +# application/vnd.dvb.notif-ia-msglist+xml +# application/vnd.dvb.notif-ia-registration-request+xml +# application/vnd.dvb.notif-ia-registration-response+xml +# application/vnd.dvb.notif-init+xml +# application/vnd.dvb.pfr +application/vnd.dvb.service svc +# application/vnd.dxr +application/vnd.dynageo geo +# application/vnd.dzr +# application/vnd.easykaraoke.cdgdownload +# application/vnd.ecdis-update +application/vnd.ecowin.chart mag +# application/vnd.ecowin.filerequest +# application/vnd.ecowin.fileupdate +# application/vnd.ecowin.series +# application/vnd.ecowin.seriesrequest +# application/vnd.ecowin.seriesupdate +# application/vnd.emclient.accessrequest+xml +application/vnd.enliven nml +# application/vnd.enphase.envoy +# application/vnd.eprints.data+xml +application/vnd.epson.esf esf +application/vnd.epson.msf msf +application/vnd.epson.quickanime qam +application/vnd.epson.salt slt +application/vnd.epson.ssf ssf +# application/vnd.ericsson.quickcall +application/vnd.eszigno3+xml es3 et3 +# application/vnd.etsi.aoc+xml +# application/vnd.etsi.asic-e+zip +# application/vnd.etsi.asic-s+zip +# application/vnd.etsi.cug+xml +# application/vnd.etsi.iptvcommand+xml +# application/vnd.etsi.iptvdiscovery+xml +# application/vnd.etsi.iptvprofile+xml +# application/vnd.etsi.iptvsad-bc+xml +# application/vnd.etsi.iptvsad-cod+xml +# application/vnd.etsi.iptvsad-npvr+xml +# application/vnd.etsi.iptvservice+xml +# application/vnd.etsi.iptvsync+xml +# application/vnd.etsi.iptvueprofile+xml +# application/vnd.etsi.mcid+xml +# application/vnd.etsi.mheg5 +# application/vnd.etsi.overload-control-policy-dataset+xml +# application/vnd.etsi.pstn+xml +# application/vnd.etsi.sci+xml +# application/vnd.etsi.simservs+xml +# application/vnd.etsi.timestamp-token +# application/vnd.etsi.tsl+xml +# application/vnd.etsi.tsl.der +# application/vnd.eudora.data +application/vnd.ezpix-album ez2 +application/vnd.ezpix-package ez3 +# application/vnd.f-secure.mobile +# application/vnd.fastcopy-disk-image +application/vnd.fdf fdf +application/vnd.fdsn.mseed mseed +application/vnd.fdsn.seed seed dataless +# application/vnd.ffsns +# application/vnd.filmit.zfc +# application/vnd.fints +# application/vnd.firemonkeys.cloudcell +application/vnd.flographit gph +application/vnd.fluxtime.clip ftc +# application/vnd.font-fontforge-sfd +application/vnd.framemaker fm frame maker book +application/vnd.frogans.fnc fnc +application/vnd.frogans.ltf ltf +application/vnd.fsc.weblaunch fsc +application/vnd.fujitsu.oasys oas +application/vnd.fujitsu.oasys2 oa2 +application/vnd.fujitsu.oasys3 oa3 +application/vnd.fujitsu.oasysgp fg5 +application/vnd.fujitsu.oasysprs bh2 +# application/vnd.fujixerox.art-ex +# application/vnd.fujixerox.art4 +application/vnd.fujixerox.ddd ddd +application/vnd.fujixerox.docuworks xdw +application/vnd.fujixerox.docuworks.binder xbd +# application/vnd.fujixerox.docuworks.container +# application/vnd.fujixerox.hbpl +# application/vnd.fut-misnet +application/vnd.fuzzysheet fzs +application/vnd.genomatix.tuxedo txd +# application/vnd.geo+json +# application/vnd.geocube+xml +application/vnd.geogebra.file ggb +application/vnd.geogebra.tool ggt +application/vnd.geometry-explorer gex gre +application/vnd.geonext gxt +application/vnd.geoplan g2w +application/vnd.geospace g3w +# application/vnd.gerber +# application/vnd.globalplatform.card-content-mgt +# application/vnd.globalplatform.card-content-mgt-response +application/vnd.gmx gmx +application/vnd.google-earth.kml+xml kml +application/vnd.google-earth.kmz kmz +# application/vnd.gov.sk.e-form+xml +# application/vnd.gov.sk.e-form+zip +# application/vnd.gov.sk.xmldatacontainer+xml +application/vnd.grafeq gqf gqs +# application/vnd.gridmp +application/vnd.groove-account gac +application/vnd.groove-help ghf +application/vnd.groove-identity-message gim +application/vnd.groove-injector grv +application/vnd.groove-tool-message gtm +application/vnd.groove-tool-template tpl +application/vnd.groove-vcard vcg +# application/vnd.hal+json +application/vnd.hal+xml hal +application/vnd.handheld-entertainment+xml zmm +application/vnd.hbci hbci +# application/vnd.hcl-bireports +# application/vnd.hdt +# application/vnd.heroku+json +application/vnd.hhe.lesson-player les +application/vnd.hp-hpgl hpgl +application/vnd.hp-hpid hpid +application/vnd.hp-hps hps +application/vnd.hp-jlyt jlt +application/vnd.hp-pcl pcl +application/vnd.hp-pclxl pclxl +# application/vnd.httphone +application/vnd.hydrostatix.sof-data sfd-hdstx +# application/vnd.hyperdrive+json +# application/vnd.hzn-3d-crossword +# application/vnd.ibm.afplinedata +# application/vnd.ibm.electronic-media +application/vnd.ibm.minipay mpy +application/vnd.ibm.modcap afp listafp list3820 +application/vnd.ibm.rights-management irm +application/vnd.ibm.secure-container sc +application/vnd.iccprofile icc icm +# application/vnd.ieee.1905 +application/vnd.igloader igl +application/vnd.immervision-ivp ivp +application/vnd.immervision-ivu ivu +# application/vnd.ims.imsccv1p1 +# application/vnd.ims.imsccv1p2 +# application/vnd.ims.imsccv1p3 +# application/vnd.ims.lis.v2.result+json +# application/vnd.ims.lti.v2.toolconsumerprofile+json +# application/vnd.ims.lti.v2.toolproxy+json +# application/vnd.ims.lti.v2.toolproxy.id+json +# application/vnd.ims.lti.v2.toolsettings+json +# application/vnd.ims.lti.v2.toolsettings.simple+json +# application/vnd.informedcontrol.rms+xml +# application/vnd.informix-visionary +# application/vnd.infotech.project +# application/vnd.infotech.project+xml +# application/vnd.innopath.wamp.notification +application/vnd.insors.igm igm +application/vnd.intercon.formnet xpw xpx +application/vnd.intergeo i2g +# application/vnd.intertrust.digibox +# application/vnd.intertrust.nncp +application/vnd.intu.qbo qbo +application/vnd.intu.qfx qfx +# application/vnd.iptc.g2.catalogitem+xml +# application/vnd.iptc.g2.conceptitem+xml +# application/vnd.iptc.g2.knowledgeitem+xml +# application/vnd.iptc.g2.newsitem+xml +# application/vnd.iptc.g2.newsmessage+xml +# application/vnd.iptc.g2.packageitem+xml +# application/vnd.iptc.g2.planningitem+xml +application/vnd.ipunplugged.rcprofile rcprofile +application/vnd.irepository.package+xml irp +application/vnd.is-xpr xpr +application/vnd.isac.fcs fcs +application/vnd.jam jam +# application/vnd.japannet-directory-service +# application/vnd.japannet-jpnstore-wakeup +# application/vnd.japannet-payment-wakeup +# application/vnd.japannet-registration +# application/vnd.japannet-registration-wakeup +# application/vnd.japannet-setstore-wakeup +# application/vnd.japannet-verification +# application/vnd.japannet-verification-wakeup +application/vnd.jcp.javame.midlet-rms rms +application/vnd.jisp jisp +application/vnd.joost.joda-archive joda +# application/vnd.jsk.isdn-ngn +application/vnd.kahootz ktz ktr +application/vnd.kde.karbon karbon +application/vnd.kde.kchart chrt +application/vnd.kde.kformula kfo +application/vnd.kde.kivio flw +application/vnd.kde.kontour kon +application/vnd.kde.kpresenter kpr kpt +application/vnd.kde.kspread ksp +application/vnd.kde.kword kwd kwt +application/vnd.kenameaapp htke +application/vnd.kidspiration kia +application/vnd.kinar kne knp +application/vnd.koan skp skd skt skm +application/vnd.kodak-descriptor sse +application/vnd.las.las+xml lasxml +# application/vnd.liberty-request+xml +application/vnd.llamagraphics.life-balance.desktop lbd +application/vnd.llamagraphics.life-balance.exchange+xml lbe +application/vnd.lotus-1-2-3 123 +application/vnd.lotus-approach apr +application/vnd.lotus-freelance pre +application/vnd.lotus-notes nsf +application/vnd.lotus-organizer org +application/vnd.lotus-screencam scm +application/vnd.lotus-wordpro lwp +application/vnd.macports.portpkg portpkg +# application/vnd.mapbox-vector-tile +# application/vnd.marlin.drm.actiontoken+xml +# application/vnd.marlin.drm.conftoken+xml +# application/vnd.marlin.drm.license+xml +# application/vnd.marlin.drm.mdcf +# application/vnd.mason+json +# application/vnd.maxmind.maxmind-db +application/vnd.mcd mcd +application/vnd.medcalcdata mc1 +application/vnd.mediastation.cdkey cdkey +# application/vnd.meridian-slingshot +application/vnd.mfer mwf +application/vnd.mfmp mfm +# application/vnd.micro+json +application/vnd.micrografx.flo flo +application/vnd.micrografx.igx igx +# application/vnd.microsoft.portable-executable +# application/vnd.miele+json +application/vnd.mif mif +# application/vnd.minisoft-hp3000-save +# application/vnd.mitsubishi.misty-guard.trustweb +application/vnd.mobius.daf daf +application/vnd.mobius.dis dis +application/vnd.mobius.mbk mbk +application/vnd.mobius.mqy mqy +application/vnd.mobius.msl msl +application/vnd.mobius.plc plc +application/vnd.mobius.txf txf +application/vnd.mophun.application mpn +application/vnd.mophun.certificate mpc +# application/vnd.motorola.flexsuite +# application/vnd.motorola.flexsuite.adsi +# application/vnd.motorola.flexsuite.fis +# application/vnd.motorola.flexsuite.gotap +# application/vnd.motorola.flexsuite.kmr +# application/vnd.motorola.flexsuite.ttc +# application/vnd.motorola.flexsuite.wem +# application/vnd.motorola.iprm +application/vnd.mozilla.xul+xml xul +# application/vnd.ms-3mfdocument +application/vnd.ms-artgalry cil +# application/vnd.ms-asf +application/vnd.ms-cab-compressed cab +# application/vnd.ms-color.iccprofile +application/vnd.ms-excel xls xlm xla xlc xlt xlw +application/vnd.ms-excel.addin.macroenabled.12 xlam +application/vnd.ms-excel.sheet.binary.macroenabled.12 xlsb +application/vnd.ms-excel.sheet.macroenabled.12 xlsm +application/vnd.ms-excel.template.macroenabled.12 xltm +application/vnd.ms-fontobject eot +application/vnd.ms-htmlhelp chm +application/vnd.ms-ims ims +application/vnd.ms-lrm lrm +# application/vnd.ms-office.activex+xml +application/vnd.ms-officetheme thmx +# application/vnd.ms-opentype +# application/vnd.ms-package.obfuscated-opentype +application/vnd.ms-pki.seccat cat +application/vnd.ms-pki.stl stl +# application/vnd.ms-playready.initiator+xml +application/vnd.ms-powerpoint ppt pps pot +application/vnd.ms-powerpoint.addin.macroenabled.12 ppam +application/vnd.ms-powerpoint.presentation.macroenabled.12 pptm +application/vnd.ms-powerpoint.slide.macroenabled.12 sldm +application/vnd.ms-powerpoint.slideshow.macroenabled.12 ppsm +application/vnd.ms-powerpoint.template.macroenabled.12 potm +# application/vnd.ms-printdevicecapabilities+xml +# application/vnd.ms-printing.printticket+xml +# application/vnd.ms-printschematicket+xml +application/vnd.ms-project mpp mpt +# application/vnd.ms-tnef +# application/vnd.ms-windows.devicepairing +# application/vnd.ms-windows.nwprinting.oob +# application/vnd.ms-windows.printerpairing +# application/vnd.ms-windows.wsd.oob +# application/vnd.ms-wmdrm.lic-chlg-req +# application/vnd.ms-wmdrm.lic-resp +# application/vnd.ms-wmdrm.meter-chlg-req +# application/vnd.ms-wmdrm.meter-resp +application/vnd.ms-word.document.macroenabled.12 docm +application/vnd.ms-word.template.macroenabled.12 dotm +application/vnd.ms-works wps wks wcm wdb +application/vnd.ms-wpl wpl +application/vnd.ms-xpsdocument xps +# application/vnd.msa-disk-image +application/vnd.mseq mseq +# application/vnd.msign +# application/vnd.multiad.creator +# application/vnd.multiad.creator.cif +# application/vnd.music-niff +application/vnd.musician mus +application/vnd.muvee.style msty +application/vnd.mynfc taglet +# application/vnd.ncd.control +# application/vnd.ncd.reference +# application/vnd.nervana +# application/vnd.netfpx +application/vnd.neurolanguage.nlu nlu +# application/vnd.nintendo.nitro.rom +# application/vnd.nintendo.snes.rom +application/vnd.nitf ntf nitf +application/vnd.noblenet-directory nnd +application/vnd.noblenet-sealer nns +application/vnd.noblenet-web nnw +# application/vnd.nokia.catalogs +# application/vnd.nokia.conml+wbxml +# application/vnd.nokia.conml+xml +# application/vnd.nokia.iptv.config+xml +# application/vnd.nokia.isds-radio-presets +# application/vnd.nokia.landmark+wbxml +# application/vnd.nokia.landmark+xml +# application/vnd.nokia.landmarkcollection+xml +# application/vnd.nokia.n-gage.ac+xml +application/vnd.nokia.n-gage.data ngdat +application/vnd.nokia.n-gage.symbian.install n-gage +# application/vnd.nokia.ncd +# application/vnd.nokia.pcd+wbxml +# application/vnd.nokia.pcd+xml +application/vnd.nokia.radio-preset rpst +application/vnd.nokia.radio-presets rpss +application/vnd.novadigm.edm edm +application/vnd.novadigm.edx edx +application/vnd.novadigm.ext ext +# application/vnd.ntt-local.content-share +# application/vnd.ntt-local.file-transfer +# application/vnd.ntt-local.ogw_remote-access +# application/vnd.ntt-local.sip-ta_remote +# application/vnd.ntt-local.sip-ta_tcp_stream +application/vnd.oasis.opendocument.chart odc +application/vnd.oasis.opendocument.chart-template otc +application/vnd.oasis.opendocument.database odb +application/vnd.oasis.opendocument.formula odf +application/vnd.oasis.opendocument.formula-template odft +application/vnd.oasis.opendocument.graphics odg +application/vnd.oasis.opendocument.graphics-template otg +application/vnd.oasis.opendocument.image odi +application/vnd.oasis.opendocument.image-template oti +application/vnd.oasis.opendocument.presentation odp +application/vnd.oasis.opendocument.presentation-template otp +application/vnd.oasis.opendocument.spreadsheet ods +application/vnd.oasis.opendocument.spreadsheet-template ots +application/vnd.oasis.opendocument.text odt +application/vnd.oasis.opendocument.text-master odm +application/vnd.oasis.opendocument.text-template ott +application/vnd.oasis.opendocument.text-web oth +# application/vnd.obn +# application/vnd.oftn.l10n+json +# application/vnd.oipf.contentaccessdownload+xml +# application/vnd.oipf.contentaccessstreaming+xml +# application/vnd.oipf.cspg-hexbinary +# application/vnd.oipf.dae.svg+xml +# application/vnd.oipf.dae.xhtml+xml +# application/vnd.oipf.mippvcontrolmessage+xml +# application/vnd.oipf.pae.gem +# application/vnd.oipf.spdiscovery+xml +# application/vnd.oipf.spdlist+xml +# application/vnd.oipf.ueprofile+xml +# application/vnd.oipf.userprofile+xml +application/vnd.olpc-sugar xo +# application/vnd.oma-scws-config +# application/vnd.oma-scws-http-request +# application/vnd.oma-scws-http-response +# application/vnd.oma.bcast.associated-procedure-parameter+xml +# application/vnd.oma.bcast.drm-trigger+xml +# application/vnd.oma.bcast.imd+xml +# application/vnd.oma.bcast.ltkm +# application/vnd.oma.bcast.notification+xml +# application/vnd.oma.bcast.provisioningtrigger +# application/vnd.oma.bcast.sgboot +# application/vnd.oma.bcast.sgdd+xml +# application/vnd.oma.bcast.sgdu +# application/vnd.oma.bcast.simple-symbol-container +# application/vnd.oma.bcast.smartcard-trigger+xml +# application/vnd.oma.bcast.sprov+xml +# application/vnd.oma.bcast.stkm +# application/vnd.oma.cab-address-book+xml +# application/vnd.oma.cab-feature-handler+xml +# application/vnd.oma.cab-pcc+xml +# application/vnd.oma.cab-subs-invite+xml +# application/vnd.oma.cab-user-prefs+xml +# application/vnd.oma.dcd +# application/vnd.oma.dcdc +application/vnd.oma.dd2+xml dd2 +# application/vnd.oma.drm.risd+xml +# application/vnd.oma.group-usage-list+xml +# application/vnd.oma.lwm2m+json +# application/vnd.oma.lwm2m+tlv +# application/vnd.oma.pal+xml +# application/vnd.oma.poc.detailed-progress-report+xml +# application/vnd.oma.poc.final-report+xml +# application/vnd.oma.poc.groups+xml +# application/vnd.oma.poc.invocation-descriptor+xml +# application/vnd.oma.poc.optimized-progress-report+xml +# application/vnd.oma.push +# application/vnd.oma.scidm.messages+xml +# application/vnd.oma.xcap-directory+xml +# application/vnd.omads-email+xml +# application/vnd.omads-file+xml +# application/vnd.omads-folder+xml +# application/vnd.omaloc-supl-init +# application/vnd.onepager +# application/vnd.openblox.game+xml +# application/vnd.openblox.game-binary +# application/vnd.openeye.oeb +application/vnd.openofficeorg.extension oxt +# application/vnd.openxmlformats-officedocument.custom-properties+xml +# application/vnd.openxmlformats-officedocument.customxmlproperties+xml +# application/vnd.openxmlformats-officedocument.drawing+xml +# application/vnd.openxmlformats-officedocument.drawingml.chart+xml +# application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramcolors+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramdata+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramlayout+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramstyle+xml +# application/vnd.openxmlformats-officedocument.extended-properties+xml +# application/vnd.openxmlformats-officedocument.presentationml.commentauthors+xml +# application/vnd.openxmlformats-officedocument.presentationml.comments+xml +# application/vnd.openxmlformats-officedocument.presentationml.handoutmaster+xml +# application/vnd.openxmlformats-officedocument.presentationml.notesmaster+xml +# application/vnd.openxmlformats-officedocument.presentationml.notesslide+xml +application/vnd.openxmlformats-officedocument.presentationml.presentation pptx +# application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml +# application/vnd.openxmlformats-officedocument.presentationml.presprops+xml +application/vnd.openxmlformats-officedocument.presentationml.slide sldx +# application/vnd.openxmlformats-officedocument.presentationml.slide+xml +# application/vnd.openxmlformats-officedocument.presentationml.slidelayout+xml +# application/vnd.openxmlformats-officedocument.presentationml.slidemaster+xml +application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx +# application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml +# application/vnd.openxmlformats-officedocument.presentationml.slideupdateinfo+xml +# application/vnd.openxmlformats-officedocument.presentationml.tablestyles+xml +# application/vnd.openxmlformats-officedocument.presentationml.tags+xml +application/vnd.openxmlformats-officedocument.presentationml.template potx +# application/vnd.openxmlformats-officedocument.presentationml.template.main+xml +# application/vnd.openxmlformats-officedocument.presentationml.viewprops+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.calcchain+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.externallink+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcachedefinition+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcacherecords+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.pivottable+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.querytable+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionheaders+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionlog+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.sharedstrings+xml +application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx +# application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.sheetmetadata+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.tablesinglecells+xml +application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx +# application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.usernames+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.volatiledependencies+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml +# application/vnd.openxmlformats-officedocument.theme+xml +# application/vnd.openxmlformats-officedocument.themeoverride+xml +# application/vnd.openxmlformats-officedocument.vmldrawing +# application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml +application/vnd.openxmlformats-officedocument.wordprocessingml.document docx +# application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.fonttable+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml +application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx +# application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.websettings+xml +# application/vnd.openxmlformats-package.core-properties+xml +# application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml +# application/vnd.openxmlformats-package.relationships+xml +# application/vnd.oracle.resource+json +# application/vnd.orange.indata +# application/vnd.osa.netdeploy +application/vnd.osgeo.mapguide.package mgp +# application/vnd.osgi.bundle +application/vnd.osgi.dp dp +application/vnd.osgi.subsystem esa +# application/vnd.otps.ct-kip+xml +# application/vnd.oxli.countgraph +# application/vnd.pagerduty+json +application/vnd.palm pdb pqa oprc +# application/vnd.panoply +# application/vnd.paos.xml +application/vnd.pawaafile paw +# application/vnd.pcos +application/vnd.pg.format str +application/vnd.pg.osasli ei6 +# application/vnd.piaccess.application-licence +application/vnd.picsel efif +application/vnd.pmi.widget wg +# application/vnd.poc.group-advertisement+xml +application/vnd.pocketlearn plf +application/vnd.powerbuilder6 pbd +# application/vnd.powerbuilder6-s +# application/vnd.powerbuilder7 +# application/vnd.powerbuilder7-s +# application/vnd.powerbuilder75 +# application/vnd.powerbuilder75-s +# application/vnd.preminet +application/vnd.previewsystems.box box +application/vnd.proteus.magazine mgz +application/vnd.publishare-delta-tree qps +application/vnd.pvi.ptid1 ptid +# application/vnd.pwg-multiplexed +# application/vnd.pwg-xhtml-print+xml +# application/vnd.qualcomm.brew-app-res +# application/vnd.quarantainenet +application/vnd.quark.quarkxpress qxd qxt qwd qwt qxl qxb +# application/vnd.quobject-quoxdocument +# application/vnd.radisys.moml+xml +# application/vnd.radisys.msml+xml +# application/vnd.radisys.msml-audit+xml +# application/vnd.radisys.msml-audit-conf+xml +# application/vnd.radisys.msml-audit-conn+xml +# application/vnd.radisys.msml-audit-dialog+xml +# application/vnd.radisys.msml-audit-stream+xml +# application/vnd.radisys.msml-conf+xml +# application/vnd.radisys.msml-dialog+xml +# application/vnd.radisys.msml-dialog-base+xml +# application/vnd.radisys.msml-dialog-fax-detect+xml +# application/vnd.radisys.msml-dialog-fax-sendrecv+xml +# application/vnd.radisys.msml-dialog-group+xml +# application/vnd.radisys.msml-dialog-speech+xml +# application/vnd.radisys.msml-dialog-transform+xml +# application/vnd.rainstor.data +# application/vnd.rapid +# application/vnd.rar +application/vnd.realvnc.bed bed +application/vnd.recordare.musicxml mxl +application/vnd.recordare.musicxml+xml musicxml +# application/vnd.renlearn.rlprint +application/vnd.rig.cryptonote cryptonote +application/vnd.rim.cod cod +application/vnd.rn-realmedia rm +application/vnd.rn-realmedia-vbr rmvb +application/vnd.route66.link66+xml link66 +# application/vnd.rs-274x +# application/vnd.ruckus.download +# application/vnd.s3sms +application/vnd.sailingtracker.track st +# application/vnd.sbm.cid +# application/vnd.sbm.mid2 +# application/vnd.scribus +# application/vnd.sealed.3df +# application/vnd.sealed.csf +# application/vnd.sealed.doc +# application/vnd.sealed.eml +# application/vnd.sealed.mht +# application/vnd.sealed.net +# application/vnd.sealed.ppt +# application/vnd.sealed.tiff +# application/vnd.sealed.xls +# application/vnd.sealedmedia.softseal.html +# application/vnd.sealedmedia.softseal.pdf +application/vnd.seemail see +application/vnd.sema sema +application/vnd.semd semd +application/vnd.semf semf +application/vnd.shana.informed.formdata ifm +application/vnd.shana.informed.formtemplate itp +application/vnd.shana.informed.interchange iif +application/vnd.shana.informed.package ipk +application/vnd.simtech-mindmapper twd twds +# application/vnd.siren+json +application/vnd.smaf mmf +# application/vnd.smart.notebook +application/vnd.smart.teacher teacher +# application/vnd.software602.filler.form+xml +# application/vnd.software602.filler.form-xml-zip +application/vnd.solent.sdkm+xml sdkm sdkd +application/vnd.spotfire.dxp dxp +application/vnd.spotfire.sfs sfs +# application/vnd.sss-cod +# application/vnd.sss-dtf +# application/vnd.sss-ntf +application/vnd.stardivision.calc sdc +application/vnd.stardivision.draw sda +application/vnd.stardivision.impress sdd +application/vnd.stardivision.math smf +application/vnd.stardivision.writer sdw vor +application/vnd.stardivision.writer-global sgl +application/vnd.stepmania.package smzip +application/vnd.stepmania.stepchart sm +# application/vnd.street-stream +# application/vnd.sun.wadl+xml +application/vnd.sun.xml.calc sxc +application/vnd.sun.xml.calc.template stc +application/vnd.sun.xml.draw sxd +application/vnd.sun.xml.draw.template std +application/vnd.sun.xml.impress sxi +application/vnd.sun.xml.impress.template sti +application/vnd.sun.xml.math sxm +application/vnd.sun.xml.writer sxw +application/vnd.sun.xml.writer.global sxg +application/vnd.sun.xml.writer.template stw +application/vnd.sus-calendar sus susp +application/vnd.svd svd +# application/vnd.swiftview-ics +application/vnd.symbian.install sis sisx +application/vnd.syncml+xml xsm +application/vnd.syncml.dm+wbxml bdm +application/vnd.syncml.dm+xml xdm +# application/vnd.syncml.dm.notification +# application/vnd.syncml.dmddf+wbxml +# application/vnd.syncml.dmddf+xml +# application/vnd.syncml.dmtnds+wbxml +# application/vnd.syncml.dmtnds+xml +# application/vnd.syncml.ds.notification +application/vnd.tao.intent-module-archive tao +application/vnd.tcpdump.pcap pcap cap dmp +# application/vnd.tmd.mediaflex.api+xml +# application/vnd.tml +application/vnd.tmobile-livetv tmo +application/vnd.trid.tpt tpt +application/vnd.triscape.mxs mxs +application/vnd.trueapp tra +# application/vnd.truedoc +# application/vnd.ubisoft.webplayer +application/vnd.ufdl ufd ufdl +application/vnd.uiq.theme utz +application/vnd.umajin umj +application/vnd.unity unityweb +application/vnd.uoml+xml uoml +# application/vnd.uplanet.alert +# application/vnd.uplanet.alert-wbxml +# application/vnd.uplanet.bearer-choice +# application/vnd.uplanet.bearer-choice-wbxml +# application/vnd.uplanet.cacheop +# application/vnd.uplanet.cacheop-wbxml +# application/vnd.uplanet.channel +# application/vnd.uplanet.channel-wbxml +# application/vnd.uplanet.list +# application/vnd.uplanet.list-wbxml +# application/vnd.uplanet.listcmd +# application/vnd.uplanet.listcmd-wbxml +# application/vnd.uplanet.signal +# application/vnd.uri-map +# application/vnd.valve.source.material +application/vnd.vcx vcx +# application/vnd.vd-study +# application/vnd.vectorworks +# application/vnd.vel+json +# application/vnd.verimatrix.vcas +# application/vnd.vidsoft.vidconference +application/vnd.visio vsd vst vss vsw +application/vnd.visionary vis +# application/vnd.vividence.scriptfile +application/vnd.vsf vsf +# application/vnd.wap.sic +# application/vnd.wap.slc +application/vnd.wap.wbxml wbxml +application/vnd.wap.wmlc wmlc +application/vnd.wap.wmlscriptc wmlsc +application/vnd.webturbo wtb +# application/vnd.wfa.p2p +# application/vnd.wfa.wsc +# application/vnd.windows.devicepairing +# application/vnd.wmc +# application/vnd.wmf.bootstrap +# application/vnd.wolfram.mathematica +# application/vnd.wolfram.mathematica.package +application/vnd.wolfram.player nbp +application/vnd.wordperfect wpd +application/vnd.wqd wqd +# application/vnd.wrq-hp3000-labelled +application/vnd.wt.stf stf +# application/vnd.wv.csp+wbxml +# application/vnd.wv.csp+xml +# application/vnd.wv.ssp+xml +# application/vnd.xacml+json +application/vnd.xara xar +application/vnd.xfdl xfdl +# application/vnd.xfdl.webform +# application/vnd.xmi+xml +# application/vnd.xmpie.cpkg +# application/vnd.xmpie.dpkg +# application/vnd.xmpie.plan +# application/vnd.xmpie.ppkg +# application/vnd.xmpie.xlim +application/vnd.yamaha.hv-dic hvd +application/vnd.yamaha.hv-script hvs +application/vnd.yamaha.hv-voice hvp +application/vnd.yamaha.openscoreformat osf +application/vnd.yamaha.openscoreformat.osfpvg+xml osfpvg +# application/vnd.yamaha.remote-setup +application/vnd.yamaha.smaf-audio saf +application/vnd.yamaha.smaf-phrase spf +# application/vnd.yamaha.through-ngn +# application/vnd.yamaha.tunnel-udpencap +# application/vnd.yaoweme +application/vnd.yellowriver-custom-menu cmp +application/vnd.zul zir zirz +application/vnd.zzazz.deck+xml zaz +application/voicexml+xml vxml +# application/vq-rtcpxr +# application/watcherinfo+xml +# application/whoispp-query +# application/whoispp-response +application/widget wgt +application/winhlp hlp +# application/wita +# application/wordperfect5.1 +application/wsdl+xml wsdl +application/wspolicy+xml wspolicy +application/x-7z-compressed 7z +application/x-abiword abw +application/x-ace-compressed ace +# application/x-amf +application/x-apple-diskimage dmg +application/x-authorware-bin aab x32 u32 vox +application/x-authorware-map aam +application/x-authorware-seg aas +application/x-bcpio bcpio +application/x-bittorrent torrent +application/x-blorb blb blorb +application/x-bzip bz +application/x-bzip2 bz2 boz +application/x-cbr cbr cba cbt cbz cb7 +application/x-cdlink vcd +application/x-cfs-compressed cfs +application/x-chat chat +application/x-chess-pgn pgn +# application/x-compress +application/x-conference nsc +application/x-cpio cpio +application/x-csh csh +application/x-debian-package deb udeb +application/x-dgc-compressed dgc +application/x-director dir dcr dxr cst cct cxt w3d fgd swa +application/x-doom wad +application/x-dtbncx+xml ncx +application/x-dtbook+xml dtb +application/x-dtbresource+xml res +application/x-dvi dvi +application/x-envoy evy +application/x-eva eva +application/x-font-bdf bdf +# application/x-font-dos +# application/x-font-framemaker +application/x-font-ghostscript gsf +# application/x-font-libgrx +application/x-font-linux-psf psf +application/x-font-pcf pcf +application/x-font-snf snf +# application/x-font-speedo +# application/x-font-sunos-news +application/x-font-type1 pfa pfb pfm afm +# application/x-font-vfont +application/x-freearc arc +application/x-futuresplash spl +application/x-gca-compressed gca +application/x-glulx ulx +application/x-gnumeric gnumeric +application/x-gramps-xml gramps +application/x-gtar gtar +# application/x-gzip +application/x-hdf hdf +application/x-install-instructions install +application/x-iso9660-image iso +application/x-java-jnlp-file jnlp +application/x-latex latex +application/x-lzh-compressed lzh lha +application/x-mie mie +application/x-mobipocket-ebook prc mobi +application/x-ms-application application +application/x-ms-shortcut lnk +application/x-ms-wmd wmd +application/x-ms-wmz wmz +application/x-ms-xbap xbap +application/x-msaccess mdb +application/x-msbinder obd +application/x-mscardfile crd +application/x-msclip clp +application/x-msdownload exe dll com bat msi +application/x-msmediaview mvb m13 m14 +application/x-msmetafile wmf wmz emf emz +application/x-msmoney mny +application/x-mspublisher pub +application/x-msschedule scd +application/x-msterminal trm +application/x-mswrite wri +application/x-netcdf nc cdf +application/x-nzb nzb +application/x-pkcs12 p12 pfx +application/x-pkcs7-certificates p7b spc +application/x-pkcs7-certreqresp p7r +application/x-rar-compressed rar +application/x-research-info-systems ris +application/x-sh sh +application/x-shar shar +application/x-shockwave-flash swf +application/x-silverlight-app xap +application/x-sql sql +application/x-stuffit sit +application/x-stuffitx sitx +application/x-subrip srt +application/x-sv4cpio sv4cpio +application/x-sv4crc sv4crc +application/x-t3vm-image t3 +application/x-tads gam +application/x-tar tar +application/x-tcl tcl +application/x-tex tex +application/x-tex-tfm tfm +application/x-texinfo texinfo texi +application/x-tgif obj +application/x-ustar ustar +application/x-wais-source src +# application/x-www-form-urlencoded +application/x-x509-ca-cert der crt +application/x-xfig fig +application/x-xliff+xml xlf +application/x-xpinstall xpi +application/x-xz xz +application/x-zmachine z1 z2 z3 z4 z5 z6 z7 z8 +# application/x400-bp +# application/xacml+xml +application/xaml+xml xaml +# application/xcap-att+xml +# application/xcap-caps+xml +application/xcap-diff+xml xdf +# application/xcap-el+xml +# application/xcap-error+xml +# application/xcap-ns+xml +# application/xcon-conference-info+xml +# application/xcon-conference-info-diff+xml +application/xenc+xml xenc +application/xhtml+xml xhtml xht +# application/xhtml-voice+xml +application/xml xml xsl +application/xml-dtd dtd +# application/xml-external-parsed-entity +# application/xml-patch+xml +# application/xmpp+xml +application/xop+xml xop +application/xproc+xml xpl +application/xslt+xml xslt +application/xspf+xml xspf +application/xv+xml mxml xhvml xvml xvm +application/yang yang +application/yin+xml yin +application/zip zip +# application/zlib +# audio/1d-interleaved-parityfec +# audio/32kadpcm +# audio/3gpp +# audio/3gpp2 +# audio/ac3 +audio/adpcm adp +# audio/amr +# audio/amr-wb +# audio/amr-wb+ +# audio/aptx +# audio/asc +# audio/atrac-advanced-lossless +# audio/atrac-x +# audio/atrac3 +audio/basic au snd +# audio/bv16 +# audio/bv32 +# audio/clearmode +# audio/cn +# audio/dat12 +# audio/dls +# audio/dsr-es201108 +# audio/dsr-es202050 +# audio/dsr-es202211 +# audio/dsr-es202212 +# audio/dv +# audio/dvi4 +# audio/eac3 +# audio/encaprtp +# audio/evrc +# audio/evrc-qcp +# audio/evrc0 +# audio/evrc1 +# audio/evrcb +# audio/evrcb0 +# audio/evrcb1 +# audio/evrcnw +# audio/evrcnw0 +# audio/evrcnw1 +# audio/evrcwb +# audio/evrcwb0 +# audio/evrcwb1 +# audio/evs +# audio/example +# audio/fwdred +# audio/g711-0 +# audio/g719 +# audio/g722 +# audio/g7221 +# audio/g723 +# audio/g726-16 +# audio/g726-24 +# audio/g726-32 +# audio/g726-40 +# audio/g728 +# audio/g729 +# audio/g7291 +# audio/g729d +# audio/g729e +# audio/gsm +# audio/gsm-efr +# audio/gsm-hr-08 +# audio/ilbc +# audio/ip-mr_v2.5 +# audio/isac +# audio/l16 +# audio/l20 +# audio/l24 +# audio/l8 +# audio/lpc +audio/midi mid midi kar rmi +# audio/mobile-xmf +audio/mp4 m4a mp4a +# audio/mp4a-latm +# audio/mpa +# audio/mpa-robust +audio/mpeg mpga mp2 mp2a mp3 m2a m3a +# audio/mpeg4-generic +# audio/musepack +audio/ogg oga ogg spx +# audio/opus +# audio/parityfec +# audio/pcma +# audio/pcma-wb +# audio/pcmu +# audio/pcmu-wb +# audio/prs.sid +# audio/qcelp +# audio/raptorfec +# audio/red +# audio/rtp-enc-aescm128 +# audio/rtp-midi +# audio/rtploopback +# audio/rtx +audio/s3m s3m +audio/silk sil +# audio/smv +# audio/smv-qcp +# audio/smv0 +# audio/sp-midi +# audio/speex +# audio/t140c +# audio/t38 +# audio/telephone-event +# audio/tone +# audio/uemclip +# audio/ulpfec +# audio/vdvi +# audio/vmr-wb +# audio/vnd.3gpp.iufp +# audio/vnd.4sb +# audio/vnd.audiokoz +# audio/vnd.celp +# audio/vnd.cisco.nse +# audio/vnd.cmles.radio-events +# audio/vnd.cns.anp1 +# audio/vnd.cns.inf1 +audio/vnd.dece.audio uva uvva +audio/vnd.digital-winds eol +# audio/vnd.dlna.adts +# audio/vnd.dolby.heaac.1 +# audio/vnd.dolby.heaac.2 +# audio/vnd.dolby.mlp +# audio/vnd.dolby.mps +# audio/vnd.dolby.pl2 +# audio/vnd.dolby.pl2x +# audio/vnd.dolby.pl2z +# audio/vnd.dolby.pulse.1 +audio/vnd.dra dra +audio/vnd.dts dts +audio/vnd.dts.hd dtshd +# audio/vnd.dvb.file +# audio/vnd.everad.plj +# audio/vnd.hns.audio +audio/vnd.lucent.voice lvp +audio/vnd.ms-playready.media.pya pya +# audio/vnd.nokia.mobile-xmf +# audio/vnd.nortel.vbk +audio/vnd.nuera.ecelp4800 ecelp4800 +audio/vnd.nuera.ecelp7470 ecelp7470 +audio/vnd.nuera.ecelp9600 ecelp9600 +# audio/vnd.octel.sbc +# audio/vnd.qcelp +# audio/vnd.rhetorex.32kadpcm +audio/vnd.rip rip +# audio/vnd.sealedmedia.softseal.mpeg +# audio/vnd.vmx.cvsd +# audio/vorbis +# audio/vorbis-config +audio/webm weba +audio/x-aac aac +audio/x-aiff aif aiff aifc +audio/x-caf caf +audio/x-flac flac +audio/x-matroska mka +audio/x-mpegurl m3u +audio/x-ms-wax wax +audio/x-ms-wma wma +audio/x-pn-realaudio ram ra +audio/x-pn-realaudio-plugin rmp +# audio/x-tta +audio/x-wav wav +audio/xm xm +chemical/x-cdx cdx +chemical/x-cif cif +chemical/x-cmdf cmdf +chemical/x-cml cml +chemical/x-csml csml +# chemical/x-pdb +chemical/x-xyz xyz +font/collection ttc +font/otf otf +# font/sfnt +font/ttf ttf +font/woff woff +font/woff2 woff2 +image/bmp bmp +image/cgm cgm +# image/dicom-rle +# image/emf +# image/example +# image/fits +image/g3fax g3 +image/gif gif +image/ief ief +# image/jls +# image/jp2 +image/jpeg jpeg jpg jpe +# image/jpm +# image/jpx +image/ktx ktx +# image/naplps +image/png png +image/prs.btif btif +# image/prs.pti +# image/pwg-raster +image/sgi sgi +image/svg+xml svg svgz +# image/t38 +image/tiff tiff tif +# image/tiff-fx +image/vnd.adobe.photoshop psd +# image/vnd.airzip.accelerator.azv +# image/vnd.cns.inf2 +image/vnd.dece.graphic uvi uvvi uvg uvvg +image/vnd.djvu djvu djv +image/vnd.dvb.subtitle sub +image/vnd.dwg dwg +image/vnd.dxf dxf +image/vnd.fastbidsheet fbs +image/vnd.fpx fpx +image/vnd.fst fst +image/vnd.fujixerox.edmics-mmr mmr +image/vnd.fujixerox.edmics-rlc rlc +# image/vnd.globalgraphics.pgb +# image/vnd.microsoft.icon +# image/vnd.mix +# image/vnd.mozilla.apng +image/vnd.ms-modi mdi +image/vnd.ms-photo wdp +image/vnd.net-fpx npx +# image/vnd.radiance +# image/vnd.sealed.png +# image/vnd.sealedmedia.softseal.gif +# image/vnd.sealedmedia.softseal.jpg +# image/vnd.svf +# image/vnd.tencent.tap +# image/vnd.valve.source.texture +image/vnd.wap.wbmp wbmp +image/vnd.xiff xif +# image/vnd.zbrush.pcx +image/webp webp +# image/wmf +image/x-3ds 3ds +image/x-cmu-raster ras +image/x-cmx cmx +image/x-freehand fh fhc fh4 fh5 fh7 +image/x-icon ico +image/x-mrsid-image sid +image/x-pcx pcx +image/x-pict pic pct +image/x-portable-anymap pnm +image/x-portable-bitmap pbm +image/x-portable-graymap pgm +image/x-portable-pixmap ppm +image/x-rgb rgb +image/x-tga tga +image/x-xbitmap xbm +image/x-xpixmap xpm +image/x-xwindowdump xwd +# message/cpim +# message/delivery-status +# message/disposition-notification +# message/example +# message/external-body +# message/feedback-report +# message/global +# message/global-delivery-status +# message/global-disposition-notification +# message/global-headers +# message/http +# message/imdn+xml +# message/news +# message/partial +message/rfc822 eml mime +# message/s-http +# message/sip +# message/sipfrag +# message/tracking-status +# message/vnd.si.simp +# message/vnd.wfa.wsc +# model/example +# model/gltf+json +model/iges igs iges +model/mesh msh mesh silo +model/vnd.collada+xml dae +model/vnd.dwf dwf +# model/vnd.flatland.3dml +model/vnd.gdl gdl +# model/vnd.gs-gdl +# model/vnd.gs.gdl +model/vnd.gtw gtw +# model/vnd.moml+xml +model/vnd.mts mts +# model/vnd.opengex +# model/vnd.parasolid.transmit.binary +# model/vnd.parasolid.transmit.text +# model/vnd.rosette.annotated-data-model +# model/vnd.valve.source.compiled-map +model/vnd.vtu vtu +model/vrml wrl vrml +model/x3d+binary x3db x3dbz +# model/x3d+fastinfoset +model/x3d+vrml x3dv x3dvz +model/x3d+xml x3d x3dz +# model/x3d-vrml +# multipart/alternative +# multipart/appledouble +# multipart/byteranges +# multipart/digest +# multipart/encrypted +# multipart/example +# multipart/form-data +# multipart/header-set +# multipart/mixed +# multipart/parallel +# multipart/related +# multipart/report +# multipart/signed +# multipart/voice-message +# multipart/x-mixed-replace +# text/1d-interleaved-parityfec +text/cache-manifest appcache +text/calendar ics ifb +text/css css +text/csv csv +# text/csv-schema +# text/directory +# text/dns +# text/ecmascript +# text/encaprtp +# text/enriched +# text/example +# text/fwdred +# text/grammar-ref-list +text/html html htm +# text/javascript +# text/jcr-cnd +# text/markdown +# text/mizar +text/n3 n3 +# text/parameters +# text/parityfec +text/plain txt text conf def list log in +# text/provenance-notation +# text/prs.fallenstein.rst +text/prs.lines.tag dsc +# text/prs.prop.logic +# text/raptorfec +# text/red +# text/rfc822-headers +text/richtext rtx +# text/rtf +# text/rtp-enc-aescm128 +# text/rtploopback +# text/rtx +text/sgml sgml sgm +# text/t140 +text/tab-separated-values tsv +text/troff t tr roff man me ms +text/turtle ttl +# text/ulpfec +text/uri-list uri uris urls +text/vcard vcard +# text/vnd.a +# text/vnd.abc +text/vnd.curl curl +text/vnd.curl.dcurl dcurl +text/vnd.curl.mcurl mcurl +text/vnd.curl.scurl scurl +# text/vnd.debian.copyright +# text/vnd.dmclientscript +text/vnd.dvb.subtitle sub +# text/vnd.esmertec.theme-descriptor +text/vnd.fly fly +text/vnd.fmi.flexstor flx +text/vnd.graphviz gv +text/vnd.in3d.3dml 3dml +text/vnd.in3d.spot spot +# text/vnd.iptc.newsml +# text/vnd.iptc.nitf +# text/vnd.latex-z +# text/vnd.motorola.reflex +# text/vnd.ms-mediapackage +# text/vnd.net2phone.commcenter.command +# text/vnd.radisys.msml-basic-layout +# text/vnd.si.uricatalogue +text/vnd.sun.j2me.app-descriptor jad +# text/vnd.trolltech.linguist +# text/vnd.wap.si +# text/vnd.wap.sl +text/vnd.wap.wml wml +text/vnd.wap.wmlscript wmls +text/x-asm s asm +text/x-c c cc cxx cpp h hh dic +text/x-fortran f for f77 f90 +text/x-java-source java +text/x-nfo nfo +text/x-opml opml +text/x-pascal p pas +text/x-setext etx +text/x-sfv sfv +text/x-uuencode uu +text/x-vcalendar vcs +text/x-vcard vcf +# text/xml +# text/xml-external-parsed-entity +# video/1d-interleaved-parityfec +video/3gpp 3gp +# video/3gpp-tt +video/3gpp2 3g2 +# video/bmpeg +# video/bt656 +# video/celb +# video/dv +# video/encaprtp +# video/example +video/h261 h261 +video/h263 h263 +# video/h263-1998 +# video/h263-2000 +video/h264 h264 +# video/h264-rcdo +# video/h264-svc +# video/h265 +# video/iso.segment +video/jpeg jpgv +# video/jpeg2000 +video/jpm jpm jpgm +video/mj2 mj2 mjp2 +# video/mp1s +# video/mp2p +# video/mp2t +video/mp4 mp4 mp4v mpg4 +# video/mp4v-es +video/mpeg mpeg mpg mpe m1v m2v +# video/mpeg4-generic +# video/mpv +# video/nv +video/ogg ogv +# video/parityfec +# video/pointer +video/quicktime qt mov +# video/raptorfec +# video/raw +# video/rtp-enc-aescm128 +# video/rtploopback +# video/rtx +# video/smpte292m +# video/ulpfec +# video/vc1 +# video/vnd.cctv +video/vnd.dece.hd uvh uvvh +video/vnd.dece.mobile uvm uvvm +# video/vnd.dece.mp4 +video/vnd.dece.pd uvp uvvp +video/vnd.dece.sd uvs uvvs +video/vnd.dece.video uvv uvvv +# video/vnd.directv.mpeg +# video/vnd.directv.mpeg-tts +# video/vnd.dlna.mpeg-tts +video/vnd.dvb.file dvb +video/vnd.fvt fvt +# video/vnd.hns.video +# video/vnd.iptvforum.1dparityfec-1010 +# video/vnd.iptvforum.1dparityfec-2005 +# video/vnd.iptvforum.2dparityfec-1010 +# video/vnd.iptvforum.2dparityfec-2005 +# video/vnd.iptvforum.ttsavc +# video/vnd.iptvforum.ttsmpeg2 +# video/vnd.motorola.video +# video/vnd.motorola.videop +video/vnd.mpegurl mxu m4u +video/vnd.ms-playready.media.pyv pyv +# video/vnd.nokia.interleaved-multimedia +# video/vnd.nokia.videovoip +# video/vnd.objectvideo +# video/vnd.radgamettools.bink +# video/vnd.radgamettools.smacker +# video/vnd.sealed.mpeg1 +# video/vnd.sealed.mpeg4 +# video/vnd.sealed.swf +# video/vnd.sealedmedia.softseal.mov +video/vnd.uvvu.mp4 uvu uvvu +video/vnd.vivo viv +# video/vp8 +video/webm webm +video/x-f4v f4v +video/x-fli fli +video/x-flv flv +video/x-m4v m4v +video/x-matroska mkv mk3d mks +video/x-mng mng +video/x-ms-asf asf asx +video/x-ms-vob vob +video/x-ms-wm wm +video/x-ms-wmv wmv +video/x-ms-wmx wmx +video/x-ms-wvx wvx +video/x-msvideo avi +video/x-sgi-movie movie +video/x-smv smv +x-conference/x-cooltalk ice diff --git a/dspace/modules/additions/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml b/dspace/modules/additions/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml index f0089dd9e474..24667d12b402 100644 --- a/dspace/modules/additions/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml +++ b/dspace/modules/additions/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml @@ -2,7 +2,9 @@ + xmlns:util="http://www.springframework.org/schema/util" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd + http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.3.xsd"> @@ -21,7 +23,6 @@ - @@ -44,7 +45,6 @@ - @@ -64,21 +64,14 @@ - - - - - - - + - - + diff --git a/dspace/modules/additions/src/test/data/dspaceFolder/config/spring/api/workflow.xml b/dspace/modules/additions/src/test/data/dspaceFolder/config/spring/api/workflow.xml index 17982626ad2d..29cd2472ff98 100644 --- a/dspace/modules/additions/src/test/data/dspaceFolder/config/spring/api/workflow.xml +++ b/dspace/modules/additions/src/test/data/dspaceFolder/config/spring/api/workflow.xml @@ -286,4 +286,4 @@ - \ No newline at end of file + diff --git a/dspace/modules/additions/src/test/java/edu/umd/lib/dspace/app/EtdLoaderTest.java b/dspace/modules/additions/src/test/java/edu/umd/lib/dspace/app/EtdLoaderTest.java index 847b5cb4ffee..194662eafb31 100644 --- a/dspace/modules/additions/src/test/java/edu/umd/lib/dspace/app/EtdLoaderTest.java +++ b/dspace/modules/additions/src/test/java/edu/umd/lib/dspace/app/EtdLoaderTest.java @@ -117,7 +117,7 @@ public void testMainEmbargoedItem() throws Exception { String logOutput = etdLogger.getLog(); assertThat(logOutput, containsString("Records written: 1")); assertThat(logOutput, containsString("Embargoes: 1")); - assertThat(logOutput, containsString("Embargoed until Tue Jun 26 00:00:00 IST 3027")); + assertThat(logOutput, containsString("Embargoed until Tue Jun 26 00:00:00 UTC 3027")); } } diff --git a/dspace/modules/pom.xml b/dspace/modules/pom.xml index 1ca970813b90..cf061a17c44f 100644 --- a/dspace/modules/pom.xml +++ b/dspace/modules/pom.xml @@ -11,7 +11,7 @@ org.dspace dspace-parent - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT ../../pom.xml diff --git a/dspace/modules/server-boot/pom.xml b/dspace/modules/server-boot/pom.xml index 77dae36e6763..acd325a91807 100644 --- a/dspace/modules/server-boot/pom.xml +++ b/dspace/modules/server-boot/pom.xml @@ -11,7 +11,7 @@ modules org.dspace - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT .. @@ -120,5 +120,4 @@ - diff --git a/dspace/modules/server-boot/src/test/java/org/dspace/app/UmdExtendedJsonAccessLogValveTest.java b/dspace/modules/server-boot/src/test/java/org/dspace/app/UmdExtendedJsonAccessLogValveTest.java index 563cff767385..fee328b0fb45 100644 --- a/dspace/modules/server-boot/src/test/java/org/dspace/app/UmdExtendedJsonAccessLogValveTest.java +++ b/dspace/modules/server-boot/src/test/java/org/dspace/app/UmdExtendedJsonAccessLogValveTest.java @@ -8,6 +8,7 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.text.SimpleDateFormat; +import java.time.Instant; import java.util.Collections; import java.util.Date; import java.util.Enumeration; @@ -244,6 +245,7 @@ private void simulateRequest( when(mockRequest.getCoyoteRequest()).thenReturn(mockCoyoteRequest); // Arbitrarily set "time" at epoch start when(mockCoyoteRequest.getStartTime()).thenReturn(0l); + when(mockCoyoteRequest.getStartInstant()).thenReturn(Instant.ofEpochSecond(0l)); // Set up the mock Response to return expected values when(mockRequest.getRemoteHost()).thenReturn(remoteIP); @@ -266,7 +268,8 @@ private void simulateRequest( when(mockRequest.getHeaders("User-Agent")).thenReturn(enumUserAgent); // Invoke the logging logic of the JsonAccessLogValve - valve.log(mockRequest, mockResponse, bytes); + long requestDuration = 42l; // Number is arbitrary + valve.log(mockRequest, mockResponse, requestDuration); } /** diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index 0b26ce5360c6..c62ba46d38d7 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -7,7 +7,7 @@ modules org.dspace - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT .. @@ -350,5 +350,4 @@ - diff --git a/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/CommunityCommunityGroupLinkRepository.java b/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/CommunityCommunityGroupLinkRepository.java index 6e6dc62cbda5..36307babd739 100644 --- a/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/CommunityCommunityGroupLinkRepository.java +++ b/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/CommunityCommunityGroupLinkRepository.java @@ -9,8 +9,8 @@ import java.sql.SQLException; import java.util.UUID; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import jakarta.servlet.http.HttpServletRequest; import org.dspace.app.rest.model.CommunityGroupRest; import org.dspace.app.rest.model.CommunityRest; diff --git a/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/CommunityGroupCommunityLinkRepository.java b/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/CommunityGroupCommunityLinkRepository.java index eb8f7dbcd30f..8b41d0caeea9 100644 --- a/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/CommunityGroupCommunityLinkRepository.java +++ b/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/CommunityGroupCommunityLinkRepository.java @@ -9,8 +9,8 @@ import java.util.LinkedList; import java.util.List; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import jakarta.servlet.http.HttpServletRequest; import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.CommunityGroupRest; diff --git a/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/EPersonLdapLinkRepository.java b/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/EPersonLdapLinkRepository.java index 760df2e31d11..92edd7cb5bfa 100644 --- a/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/EPersonLdapLinkRepository.java +++ b/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/EPersonLdapLinkRepository.java @@ -4,12 +4,12 @@ import java.util.List; import java.util.UUID; import java.util.stream.Collectors; -import javax.annotation.Nullable; import javax.naming.NamingException; import edu.umd.lib.dspace.authenticate.LdapService; import edu.umd.lib.dspace.authenticate.impl.Ldap; import edu.umd.lib.dspace.authenticate.impl.LdapServiceImpl; +import jakarta.annotation.Nullable; import jakarta.servlet.http.HttpServletRequest; import org.dspace.app.rest.model.EPersonRest; import org.dspace.app.rest.model.GroupRest; diff --git a/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/EtdUnitCollectionLinkRepository.java b/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/EtdUnitCollectionLinkRepository.java index a9ea3a815d24..6bdd2184c912 100644 --- a/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/EtdUnitCollectionLinkRepository.java +++ b/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/EtdUnitCollectionLinkRepository.java @@ -2,8 +2,8 @@ import java.sql.SQLException; import java.util.UUID; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import jakarta.servlet.http.HttpServletRequest; import org.dspace.app.rest.model.CollectionRest; import org.dspace.app.rest.model.EtdUnitRest; diff --git a/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/UnitGroupLinkRepository.java b/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/UnitGroupLinkRepository.java index b6e422672763..736696e9e27b 100644 --- a/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/UnitGroupLinkRepository.java +++ b/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/UnitGroupLinkRepository.java @@ -2,8 +2,8 @@ import java.sql.SQLException; import java.util.UUID; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import jakarta.servlet.http.HttpServletRequest; import org.dspace.app.rest.model.GroupRest; import org.dspace.app.rest.model.UnitRest; diff --git a/dspace/pom.xml b/dspace/pom.xml index 8643a13f0bc0..19ee9d5f27a7 100644 --- a/dspace/pom.xml +++ b/dspace/pom.xml @@ -16,7 +16,7 @@ org.dspace dspace-parent - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT ../pom.xml diff --git a/dspace/solr/search/conf/schema.xml b/dspace/solr/search/conf/schema.xml index 24f667d7fc3f..c07d589d7fa2 100644 --- a/dspace/solr/search/conf/schema.xml +++ b/dspace/solr/search/conf/schema.xml @@ -329,7 +329,7 @@ - + diff --git a/dspace/solr/search/conf/solrconfig.xml b/dspace/solr/search/conf/solrconfig.xml index 97b1d1ddbbf6..71c6c8846941 100644 --- a/dspace/solr/search/conf/solrconfig.xml +++ b/dspace/solr/search/conf/solrconfig.xml @@ -148,6 +148,12 @@ + + + + + + false diff --git a/dspace/src/main/docker-compose/cli.assetstore.yml b/dspace/src/main/docker-compose/cli.assetstore.yml index 6563aa081eb1..ab9527f8e4af 100644 --- a/dspace/src/main/docker-compose/cli.assetstore.yml +++ b/dspace/src/main/docker-compose/cli.assetstore.yml @@ -10,7 +10,7 @@ services: dspace-cli: environment: # This assetstore zip is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data - - LOADASSETS=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/assetstore.tar.gz + - LOADASSETS=${LOADASSETS:-https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/assetstore.tar.gz} entrypoint: - /bin/bash - '-c' diff --git a/dspace/src/main/docker-compose/cli.ingest.yml b/dspace/src/main/docker-compose/cli.ingest.yml index 6172147f1955..b6b9886eb730 100644 --- a/dspace/src/main/docker-compose/cli.ingest.yml +++ b/dspace/src/main/docker-compose/cli.ingest.yml @@ -9,9 +9,9 @@ services: dspace-cli: environment: - - AIPZIP=https://github.com/DSpace-Labs/AIP-Files/raw/main/dogAndReport.zip - - ADMIN_EMAIL=test@test.edu - - AIPDIR=/tmp/aip-dir + - AIPZIP=${AIPZIP:-https://github.com/DSpace-Labs/AIP-Files/raw/main/dogAndReport.zip} + - ADMIN_EMAIL=${ADMIN_EMAIL:-test@test.edu} + - AIPDIR=${AIPDIR:-/tmp/aip-dir} entrypoint: - /bin/bash - '-c' diff --git a/dspace/src/main/docker-compose/db.entities.yml b/dspace/src/main/docker-compose/db.entities.yml index 15c7496090c9..6666a71ce458 100644 --- a/dspace/src/main/docker-compose/db.entities.yml +++ b/dspace/src/main/docker-compose/db.entities.yml @@ -9,11 +9,11 @@ services: dspacedb: # UMD Customization - image: dspace/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql" # End UMD Customization environment: # This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data - - LOADSQL=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql + - LOADSQL=${LOADSQL:-https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql} dspace: ### OVERRIDE default 'entrypoint' in 'docker-compose.yml #### # Ensure that the database is ready BEFORE starting tomcat @@ -26,4 +26,4 @@ services: - | while (! /dev/null 2>&1; do sleep 1; done; /dspace/bin/dspace database migrate ignored - java -jar /dspace/webapps/server-boot.jar --dspace.dir=/dspace + java -jar /dspace/webapps/server-boot.jar diff --git a/dspace/src/main/docker-compose/db.restore.yml b/dspace/src/main/docker-compose/db.restore.yml index 1353647b8b27..8facf409f374 100644 --- a/dspace/src/main/docker-compose/db.restore.yml +++ b/dspace/src/main/docker-compose/db.restore.yml @@ -13,11 +13,11 @@ services: dspacedb: # UMD Customization - image: dspace/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql" # End UMD Customization environment: # Location where the dump SQL file will be available on the running container - - LOCALSQL=/tmp/pgdump.sql + - LOCALSQL=${LOCALSQL:-/tmp/pgdump.sql} volumes: # Volume which shares a local SQL file at "./pgdump.sql" to the running container # IF YOUR LOCAL FILE HAS A DIFFERENT NAME (or is in a different location), then change the "./pgdump.sql" diff --git a/dspace/src/main/docker-compose/docker-compose-angular.yml b/dspace/src/main/docker-compose/docker-compose-angular.yml index 3610d23286b8..253c37114ffb 100644 --- a/dspace/src/main/docker-compose/docker-compose-angular.yml +++ b/dspace/src/main/docker-compose/docker-compose-angular.yml @@ -18,19 +18,18 @@ services: depends_on: - dspace environment: - DSPACE_UI_SSL: 'false' - DSPACE_UI_HOST: dspace-angular - DSPACE_UI_PORT: '4000' - DSPACE_UI_NAMESPACE: / - DSPACE_REST_SSL: 'false' - DSPACE_REST_HOST: localhost - DSPACE_REST_PORT: 8080 - DSPACE_REST_NAMESPACE: /server - image: dspace/dspace-angular:${DSPACE_VER:-dspace-8_x} + DSPACE_UI_SSL: ${DSPACE_UI_SSL:-false} + DSPACE_UI_HOST: ${DSPACE_UI_HOST:-dspace-angular} + DSPACE_UI_PORT: ${DSPACE_UI_PORT:-4000} + DSPACE_UI_NAMESPACE: ${DSPACE_UI_NAMESPACE:-/} + DSPACE_UI_BASEURL: ${DSPACE_UI_BASEURL:-http://localhost:4000} + DSPACE_REST_SSL: ${DSPACE_REST_SSL:-false} + DSPACE_REST_HOST: ${DSPACE_REST_HOST:-localhost} + DSPACE_REST_PORT: ${DSPACE_REST_PORT:-8080} + DSPACE_REST_NAMESPACE: ${DSPACE_REST_NAMESPACE:-/server} + # Ensure SSR can use the 'dspace' Docker image directly (see docker-compose-rest.yml) + DSPACE_REST_SSRBASEURL: ${DSPACE_REST_SSRBASEURL:-http://dspace:8080/server} + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-dspace-8_x-dist}" ports: - published: 4000 target: 4000 - - published: 9876 - target: 9876 - stdin_open: true - tty: true diff --git a/dspace/src/main/docker-compose/docker-compose-shibboleth.yml b/dspace/src/main/docker-compose/docker-compose-shibboleth.yml index f7fb2dcbd1ae..1290fbc1e909 100644 --- a/dspace/src/main/docker-compose/docker-compose-shibboleth.yml +++ b/dspace/src/main/docker-compose/docker-compose-shibboleth.yml @@ -21,7 +21,7 @@ services: container_name: dspace-shibboleth depends_on: - dspace - image: dspace/dspace-shibboleth + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-shibboleth" build: # Must be relative to root, so that it can be built alongside [src]/docker-compose.yml context: ./dspace/src/main/docker/dspace-shibboleth @@ -30,8 +30,6 @@ services: target: 80 - published: 443 target: 443 - stdin_open: true - tty: true environment: # Default to using "localhost" for Apache & Shibboleth # However, you can override this via the "DSPACE_HOSTNAME" environment variable. diff --git a/pom.xml b/pom.xml index 0a04982a1549..48ea1737aca1 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.dspace dspace-parent pom - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT DSpace Parent Project DSpace open source software is a turnkey institutional repository application. @@ -19,39 +19,42 @@ 17 - 6.2.8 - 3.5.3 - 6.5.1 - 6.4.8.Final - 8.0.2.Final - 42.7.7 + 6.2.18 + 3.3.7 + 3.5.14 + 6.5.10 + 6.4.10.Final + 8.0.3.Final + 42.7.11 10.22.0 8.11.4 - 3.10.8 - 2.38.0 - - 2.19.1 - 2.19.1 + 3.12.0 + 2.42.0 + + 2.21.3 + 2.21 2.1.1 - 4.0.2 - 4.0.5 + 4.0.5 + 4.0.8 1.1.1 9.4.58.v20250814 - 2.25.2 - 3.0.5 + 2.25.4 + 3.0.7 1.19.0 2.0.17 - 3.2.3 + 3.3.0 + 1.81 8.0.1 - 3.1.10 + 3.1.11 - 2.9.0 + 2.10.0 9.48 @@ -89,7 +92,7 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.5.0 + 3.6.2 enforce-java @@ -128,7 +131,16 @@ + log4j:log4j + + javax.annotation:javax.annotation-api + javax.inject:javax.inject + javax.mail:mail + com.sun.mail:javax.mail + javax.persistence:javax.persistence-api + javax.servlet:servlet-api + javax.validation:validation-api @@ -140,7 +152,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.14.0 + 3.15.0 ${java.version} @@ -177,7 +189,7 @@ org.apache.maven.plugins maven-jar-plugin - 3.4.2 + 3.5.0 @@ -191,7 +203,7 @@ org.apache.maven.plugins maven-war-plugin - 3.4.0 + 3.5.1 false @@ -208,7 +220,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.5.3 + 3.5.5 @@ -235,7 +247,7 @@ maven-failsafe-plugin - 3.5.3 + 3.5.5 @@ -303,7 +315,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.9.3.0 + 4.9.8.3 Max Low @@ -313,7 +325,7 @@ com.github.spotbugs spotbugs - 4.9.3 + 4.9.8 @@ -343,17 +355,17 @@ maven-assembly-plugin - 3.7.1 + 3.8.0 org.apache.maven.plugins maven-dependency-plugin - 3.8.1 + 3.10.0 org.apache.maven.plugins maven-resources-plugin - 3.3.1 + 3.5.0 @@ -365,13 +377,13 @@ org.sonatype.central central-publishing-maven-plugin - 0.8.0 + 0.10.0 org.apache.maven.plugins maven-javadoc-plugin - 3.11.2 + 3.12.0 false @@ -386,7 +398,7 @@ org.apache.maven.plugins maven-source-plugin - 3.3.1 + 3.4.0 @@ -399,13 +411,13 @@ org.apache.maven.plugins maven-gpg-plugin - 3.2.7 + 3.2.8 org.jacoco jacoco-maven-plugin - 0.8.13 + 0.8.14 @@ -417,7 +429,7 @@ org.apache.maven.plugins maven-release-plugin - 3.1.1 + 3.3.1 @@ -446,8 +458,6 @@ - - **/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationPropertySource.java **/src/test/resources/** **/src/test/data/** **/src/main/license/** @@ -485,7 +495,7 @@ org.codehaus.mojo xml-maven-plugin - 1.1.0 + 1.2.1 validate-ALL-xml-and-xsl @@ -528,10 +538,10 @@ - + test-argLine @@ -540,7 +550,7 @@ - -Xmx1024m + -Xmx1024m -Dfile.encoding=UTF-8 @@ -713,7 +723,7 @@ org.codehaus.mojo license-maven-plugin - 2.5.0 + 2.7.1 false @@ -1036,68 +1046,68 @@ org.dspace dspace-api - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT org.dspace dspace-api test-jar - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT test org.dspace.modules additions - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT org.dspace.modules server classes - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT org.dspace dspace-sword - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT org.dspace dspace-swordv2 - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT org.dspace dspace-oai - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT org.dspace dspace-services - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT org.dspace dspace-server-webapp test-jar - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT test org.dspace dspace-rdf - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT org.dspace dspace-iiif - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT org.dspace dspace-server-webapp - 8.1-drum-3-SNAPSHOT + 8.4-drum-0-SNAPSHOT @@ -1145,11 +1155,21 @@ ${hibernate-validator.version} - + org.jboss.logging jboss-logging - 3.6.1.Final + 3.5.0.Final @@ -1180,13 +1200,6 @@ org.springframework spring-orm ${spring.version} - - - - org.springframework - spring-jcl - - @@ -1194,6 +1207,14 @@ spring-core org.springframework ${spring.version} + + + + org.springframework + spring-jcl + + @@ -1220,6 +1241,25 @@ ${spring.version} + + org.springframework.ldap + spring-ldap-core + ${spring-ldap.version} + + + + io.micrometer + micrometer-core + + + io.micrometer + micrometer-observation + + + + spring-tx org.springframework @@ -1334,13 +1374,13 @@ com.healthmarketscience.jackcess jackcess - 4.0.8 + 4.0.10 org.apache.james apache-mime4j-core - 0.8.12 + 0.8.14 @@ -1364,19 +1404,12 @@ ${asm.version} - - - org.checkerframework - checker-qual - 3.49.5 - - com.google.code.gson gson - 2.13.1 + 2.14.0 @@ -1523,49 +1556,49 @@ commons-io commons-io - 2.19.0 + 2.22.0 org.apache.commons commons-lang3 - 3.17.0 + 3.20.0 commons-logging commons-logging - 1.3.5 + 1.3.6 org.apache.commons commons-compress - 1.27.1 + 1.28.0 org.apache.commons commons-csv - 1.14.0 + 1.14.1 org.apache.commons commons-pool2 - 2.12.1 + 2.13.1 org.apache.commons commons-text - 1.13.1 + 1.15.0 commons-validator commons-validator - 1.9.0 + 1.10.1 jakarta.activation jakarta.activation-api - 2.1.3 + 2.1.4 @@ -1577,14 +1610,14 @@ jakarta.mail jakarta.mail-api - 2.1.3 + 2.1.5 provided org.eclipse.angus jakarta.mail - 2.0.3 + 2.0.5 jakarta.servlet @@ -1596,7 +1629,7 @@ jaxen jaxen - 2.0.0 + 2.0.1 org.jdom @@ -1730,7 +1763,7 @@ com.h2database h2 - 2.3.232 + 2.4.240 test @@ -1758,12 +1791,12 @@ com.fasterxml classmate - 1.7.0 + 1.7.3 com.fasterxml.jackson.core jackson-annotations - ${jackson.version} + ${jackson-annotations.version} com.fasterxml.jackson.core @@ -1773,19 +1806,19 @@ com.fasterxml.jackson.core jackson-databind - ${jackson-databind.version} + ${jackson.version} com.fasterxml.jackson.datatype jackson-datatype-jsr310 - ${jackson-databind.version} + ${jackson.version} com.google.guava guava - 32.1.3-jre + 33.6.0-jre - + org.checkerframework checker-qual @@ -1795,7 +1828,7 @@ xom xom - 1.3.9 + 1.4.1 xml-apis @@ -1937,7 +1970,7 @@ scm:git:git@github.com:DSpace/DSpace.git scm:git:git@github.com:DSpace/DSpace.git https://github.com/DSpace/DSpace - dspace-8.2 + dspace-8.4