diff --git a/.env b/.env new file mode 100644 index 00000000000..3467f8df73b --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +APP_IMAGE=gdcc/dataverse:unstable +POSTGRES_VERSION=13 +DATAVERSE_DB_USER=dataverse +SOLR_VERSION=8.11.1 diff --git a/.github/workflows/container_app_pr.yml b/.github/workflows/container_app_pr.yml new file mode 100644 index 00000000000..9e514690a13 --- /dev/null +++ b/.github/workflows/container_app_pr.yml @@ -0,0 +1,96 @@ +--- +name: Preview Application Container Image + +on: + # We only run the push commands if we are asked to by an issue comment with the correct command. + # This workflow is always taken from the default branch and runs in repo context with access to secrets. + repository_dispatch: + types: [ push-image-command ] + +env: + IMAGE_TAG: unstable + BASE_IMAGE_TAG: unstable + PLATFORMS: "linux/amd64,linux/arm64" + +jobs: + deploy: + name: "Package & Push" + runs-on: ubuntu-latest + # Only run in upstream repo - avoid unnecessary runs in forks + if: ${{ github.repository_owner == 'IQSS' }} + steps: + # Checkout the pull request code as when merged + - uses: actions/checkout@v3 + with: + ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge' + - uses: actions/setup-java@v3 + with: + java-version: "11" + distribution: 'adopt' + - uses: actions/cache@v3 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + + # Note: Accessing, pushing tags etc. to GHCR will only succeed in upstream because secrets. + - name: Login to Github Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ secrets.GHCR_USERNAME }} + password: ${{ secrets.GHCR_TOKEN }} + + - name: Set up QEMU for multi-arch builds + uses: docker/setup-qemu-action@v2 + + # Get the image tag from either the command or default to branch name (Not used for now) + #- name: Get the target tag name + # id: vars + # run: | + # tag=${{ github.event.client_payload.slash_command.args.named.tag }} + # if [[ -z "$tag" ]]; then tag=$(echo "${{ github.event.client_payload.pull_request.head.ref }}" | tr '\\/_:&+,;#*' '-'); fi + # echo "IMAGE_TAG=$tag" >> $GITHUB_ENV + + # Set image tag to branch name of the PR + - name: Set image tag to branch name + run: | + echo "IMAGE_TAG=$(echo "${{ github.event.client_payload.pull_request.head.ref }}" | tr '\\/_:&+,;#*' '-')" >> $GITHUB_ENV + + # Necessary to split as otherwise the submodules are not available (deploy skips install) + - name: Build app and configbaker container image with local architecture and submodules (profile will skip tests) + run: > + mvn -B -f modules/dataverse-parent + -P ct -pl edu.harvard.iq:dataverse -am + install + - name: Deploy multi-arch application and configbaker container image + run: > + mvn + -Dapp.image.tag=${{ env.IMAGE_TAG }} -Dbase.image.tag=${{ env.BASE_IMAGE_TAG }} + -Ddocker.registry=ghcr.io -Ddocker.platforms=${{ env.PLATFORMS }} + -Pct deploy + + - uses: marocchino/sticky-pull-request-comment@v2 + with: + header: registry-push + hide_and_recreate: true + hide_classify: "OUTDATED" + number: ${{ github.event.client_payload.pull_request.number }} + message: | + :package: Pushed preview images as + ``` + ghcr.io/gdcc/dataverse:${{ env.IMAGE_TAG }} + ``` + ``` + ghcr.io/gdcc/configbaker:${{ env.IMAGE_TAG }} + ``` + :ship: [See on GHCR](https://github.com/orgs/gdcc/packages/container). Use by referencing with full name as printed above, mind the registry name. + + # Leave a note when things have gone sideways + - uses: peter-evans/create-or-update-comment@v3 + if: ${{ failure() }} + with: + issue-number: ${{ github.event.client_payload.pull_request.number }} + body: > + :package: Could not push preview images :disappointed:. + See [log](https://github.com/IQSS/dataverse/actions/runs/${{ github.run_id }}) for details. diff --git a/.github/workflows/container_app_push.yml b/.github/workflows/container_app_push.yml new file mode 100644 index 00000000000..c60691b1c85 --- /dev/null +++ b/.github/workflows/container_app_push.yml @@ -0,0 +1,167 @@ +--- +name: Application Container Image + +on: + # We are deliberately *not* running on push events here to avoid double runs. + # Instead, push events will trigger from the base image and maven unit tests via workflow_call. + workflow_call: + pull_request: + branches: + - develop + - master + paths: + - 'src/main/docker/**' + - 'modules/container-configbaker/**' + - '.github/workflows/container_app_push.yml' + +env: + IMAGE_TAG: unstable + BASE_IMAGE_TAG: unstable + REGISTRY: "" # Empty means default to Docker Hub + PLATFORMS: "linux/amd64,linux/arm64" + MASTER_BRANCH_TAG: alpha + +jobs: + build: + name: "Build & Test" + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + pull-requests: write + # Only run in upstream repo - avoid unnecessary runs in forks + if: ${{ github.repository_owner == 'IQSS' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: "11" + distribution: temurin + cache: maven + + - name: Build app and configbaker container image with local architecture and submodules (profile will skip tests) + run: > + mvn -B -f modules/dataverse-parent + -P ct -pl edu.harvard.iq:dataverse -am + install + + # TODO: add smoke / integration testing here (add "-Pct -DskipIntegrationTests=false") + + hub-description: + needs: build + name: Push image descriptions to Docker Hub + # Run this when triggered via push or schedule as reused workflow from base / maven unit tests. + # Excluding PRs here means we will have no trouble with secrets access. Also avoid runs in forks. + if: ${{ github.event_name != 'pull_request' && github.ref_name == 'develop' && github.repository_owner == 'IQSS' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: peter-evans/dockerhub-description@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: gdcc/dataverse + short-description: "Dataverse Application Container Image providing the executable" + readme-filepath: ./src/main/docker/README.md + - uses: peter-evans/dockerhub-description@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: gdcc/configbaker + short-description: "Dataverse Config Baker Container Image providing setup tooling and more" + readme-filepath: ./modules/container-configbaker/README.md + + # Note: Accessing, pushing tags etc. to DockerHub or GHCR will only succeed in upstream because secrets. + # We check for them here and subsequent jobs can rely on this to decide if they shall run. + check-secrets: + needs: build + name: Check for Secrets Availability + runs-on: ubuntu-latest + outputs: + available: ${{ steps.secret-check.outputs.available }} + steps: + - id: secret-check + # perform secret check & put boolean result as an output + shell: bash + run: | + if [ "${{ secrets.DOCKERHUB_TOKEN }}" != '' ]; then + echo "available=true" >> $GITHUB_OUTPUT; + else + echo "available=false" >> $GITHUB_OUTPUT; + fi + + deploy: + needs: check-secrets + name: "Package & Publish" + runs-on: ubuntu-latest + # Only run this job if we have access to secrets. This is true for events like push/schedule which run in + # context of main repo, but for PRs only true if coming from the main repo! Forks have no secret access. + if: needs.check-secrets.outputs.available == 'true' + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + java-version: "11" + distribution: temurin + + # Depending on context, we push to different targets. Login accordingly. + - if: ${{ github.event_name != 'pull_request' }} + name: Log in to Docker Hub registry + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - if: ${{ github.event_name == 'pull_request' }} + name: Login to Github Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ secrets.GHCR_USERNAME }} + password: ${{ secrets.GHCR_TOKEN }} + + - name: Set up QEMU for multi-arch builds + uses: docker/setup-qemu-action@v2 + + - name: Re-set image tag based on branch (if master) + if: ${{ github.ref_name == 'master' }} + run: | + echo "IMAGE_TAG=${{ env.MASTER_BRANCH_TAG }}" >> $GITHUB_ENV + echo "BASE_IMAGE_TAG=${{ env.MASTER_BRANCH_TAG }}" >> $GITHUB_ENV + - name: Re-set image tag and container registry when on PR + if: ${{ github.event_name == 'pull_request' }} + run: | + echo "IMAGE_TAG=$(echo "$GITHUB_HEAD_REF" | tr '\\/_:&+,;#*' '-')" >> $GITHUB_ENV + echo "REGISTRY='-Ddocker.registry=ghcr.io'" >> $GITHUB_ENV + + # Necessary to split as otherwise the submodules are not available (deploy skips install) + - name: Build app and configbaker container image with local architecture and submodules (profile will skip tests) + run: > + mvn -B -f modules/dataverse-parent + -P ct -pl edu.harvard.iq:dataverse -am + install + - name: Deploy multi-arch application and configbaker container image + run: > + mvn + -Dapp.image.tag=${{ env.IMAGE_TAG }} -Dbase.image.tag=${{ env.BASE_IMAGE_TAG }} + ${{ env.REGISTRY }} -Ddocker.platforms=${{ env.PLATFORMS }} + -P ct deploy + + - uses: marocchino/sticky-pull-request-comment@v2 + if: ${{ github.event_name == 'pull_request' }} + with: + header: registry-push + hide_and_recreate: true + hide_classify: "OUTDATED" + message: | + :package: Pushed preview images as + ``` + ghcr.io/gdcc/dataverse:${{ env.IMAGE_TAG }} + ``` + ``` + ghcr.io/gdcc/configbaker:${{ env.IMAGE_TAG }} + ``` + :ship: [See on GHCR](https://github.com/orgs/gdcc/packages/container). Use by referencing with full name as printed above, mind the registry name. diff --git a/.github/workflows/container_base_push.yml b/.github/workflows/container_base_push.yml index 8f440151d0c..5c62fb0c811 100644 --- a/.github/workflows/container_base_push.yml +++ b/.github/workflows/container_base_push.yml @@ -1,5 +1,5 @@ --- -name: Container Base Module +name: Base Container Image on: push: @@ -18,9 +18,12 @@ on: - 'modules/container-base/**' - 'modules/dataverse-parent/pom.xml' - '.github/workflows/container_base_push.yml' + schedule: + - cron: '23 3 * * 0' # Run for 'develop' every Sunday at 03:23 UTC env: IMAGE_TAG: unstable + PLATFORMS: linux/amd64,linux/arm64 jobs: build: @@ -79,7 +82,18 @@ jobs: uses: docker/setup-qemu-action@v2 - name: Re-set image tag based on branch if: ${{ github.ref_name == 'master' }} - run: echo "IMAGE_TAG=stable" + run: echo "IMAGE_TAG=alpha" >> $GITHUB_ENV - if: ${{ github.event_name != 'pull_request' }} name: Deploy multi-arch base container image to Docker Hub - run: mvn -f modules/container-base -Pct deploy -Dbase.image.tag=${{ env.IMAGE_TAG }} + run: mvn -f modules/container-base -Pct deploy -Dbase.image.tag=${{ env.IMAGE_TAG }} -Ddocker.platforms=${{ env.PLATFORMS }} + push-app-img: + name: "Rebase & Publish App Image" + permissions: + contents: read + packages: write + pull-requests: write + needs: build + # We do not release a new base image for pull requests, so do not trigger. + if: ${{ github.event_name != 'pull_request' }} + uses: ./.github/workflows/container_app_push.yml + secrets: inherit diff --git a/.github/workflows/deploy_beta_testing.yml b/.github/workflows/deploy_beta_testing.yml new file mode 100644 index 00000000000..3e67bfe426e --- /dev/null +++ b/.github/workflows/deploy_beta_testing.yml @@ -0,0 +1,80 @@ +name: 'Deploy to Beta Testing' + +on: + push: + branches: + - develop + +jobs: + build: + runs-on: ubuntu-latest + environment: beta-testing + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '11' + + - name: Build application war + run: mvn package + + - name: Get war file name + working-directory: target + run: echo "war_file=$(ls *.war | head -1)">> $GITHUB_ENV + + - name: Upload war artifact + uses: actions/upload-artifact@v3 + with: + name: built-app + path: ./target/${{ env.war_file }} + + deploy-to-payara: + needs: build + runs-on: ubuntu-latest + environment: beta-testing + + steps: + - uses: actions/checkout@v3 + + - name: Download war artifact + uses: actions/download-artifact@v3 + with: + name: built-app + path: ./ + + - name: Get war file name + run: echo "war_file=$(ls *.war | head -1)">> $GITHUB_ENV + + - name: Copy war file to remote instance + uses: appleboy/scp-action@master + with: + host: ${{ secrets.PAYARA_INSTANCE_HOST }} + username: ${{ secrets.PAYARA_INSTANCE_USERNAME }} + key: ${{ secrets.PAYARA_INSTANCE_SSH_PRIVATE_KEY }} + source: './${{ env.war_file }}' + target: '/home/${{ secrets.PAYARA_INSTANCE_USERNAME }}' + overwrite: true + + - name: Execute payara war deployment remotely + uses: appleboy/ssh-action@v1.0.0 + env: + INPUT_WAR_FILE: ${{ env.war_file }} + with: + host: ${{ secrets.PAYARA_INSTANCE_HOST }} + username: ${{ secrets.PAYARA_INSTANCE_USERNAME }} + key: ${{ secrets.PAYARA_INSTANCE_SSH_PRIVATE_KEY }} + envs: INPUT_WAR_FILE + script: | + APPLICATION_NAME=dataverse-backend + ASADMIN='/usr/local/payara5/bin/asadmin --user admin' + $ASADMIN undeploy $APPLICATION_NAME + $ASADMIN stop-domain + rm -rf /usr/local/payara5/glassfish/domains/domain1/generated + rm -rf /usr/local/payara5/glassfish/domains/domain1/osgi-cache + $ASADMIN start-domain + $ASADMIN deploy --name $APPLICATION_NAME $INPUT_WAR_FILE + $ASADMIN stop-domain + $ASADMIN start-domain diff --git a/.github/workflows/maven_unit_test.yml b/.github/workflows/maven_unit_test.yml index e2048f73431..45beabf3193 100644 --- a/.github/workflows/maven_unit_test.yml +++ b/.github/workflows/maven_unit_test.yml @@ -6,11 +6,15 @@ on: - "**.java" - "pom.xml" - "modules/**/pom.xml" + - "!modules/container-base/**" + - "!modules/dataverse-spi/**" pull_request: paths: - "**.java" - "pom.xml" - "modules/**/pom.xml" + - "!modules/container-base/**" + - "!modules/dataverse-spi/**" jobs: unittest: @@ -33,22 +37,43 @@ jobs: continue-on-error: ${{ matrix.experimental }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up JDK ${{ matrix.jdk }} - uses: actions/setup-java@v2 + uses: actions/setup-java@v3 with: java-version: ${{ matrix.jdk }} - distribution: 'adopt' - - name: Cache Maven packages - uses: actions/cache@v2 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + distribution: temurin + cache: maven + + # The reason why we use "install" here is that we want the submodules to be available in the next step. + # Also, we can cache them this way for jobs triggered by this one. - name: Build with Maven - run: mvn -DcompilerArgument=-Xlint:unchecked -Dtarget.java.version=${{ matrix.jdk }} -P all-unit-tests clean test + run: > + mvn -B -f modules/dataverse-parent + -Dtarget.java.version=${{ matrix.jdk }} + -DcompilerArgument=-Xlint:unchecked -P all-unit-tests + -pl edu.harvard.iq:dataverse -am + install + - name: Maven Code Coverage env: CI_NAME: github COVERALLS_SECRET: ${{ secrets.GITHUB_TOKEN }} - run: mvn -V -B jacoco:report coveralls:report -DrepoToken=${COVERALLS_SECRET} -DpullRequest=${{ github.event.number }} \ No newline at end of file + # The coverage commit is sometimes flaky. Don't bail out just because this optional step failed. + continue-on-error: true + run: > + mvn -B + -DrepoToken=${COVERALLS_SECRET} -DpullRequest=${{ github.event.number }} + jacoco:report coveralls:report + + # We don't want to cache the WAR file, so delete it + - run: rm -rf ~/.m2/repository/edu/harvard/iq/dataverse + push-app-img: + name: Publish App Image + permissions: + contents: read + packages: write + pull-requests: write + needs: unittest + uses: ./.github/workflows/container_app_push.yml + secrets: inherit diff --git a/.github/workflows/pr_comment_commands.yml b/.github/workflows/pr_comment_commands.yml new file mode 100644 index 00000000000..5ff75def623 --- /dev/null +++ b/.github/workflows/pr_comment_commands.yml @@ -0,0 +1,20 @@ +name: PR Comment Commands +on: + issue_comment: + types: [created] +jobs: + dispatch: + # Avoid being triggered by forks in upstream + if: ${{ github.repository_owner == 'IQSS' }} + runs-on: ubuntu-latest + steps: + - name: Dispatch + uses: peter-evans/slash-command-dispatch@v3 + with: + # This token belongs to @dataversebot and has sufficient scope. + token: ${{ secrets.GHCR_TOKEN }} + commands: | + push-image + repository: IQSS/dataverse + # Commenter must have at least write permission to repo to trigger dispatch + permission: write diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index 2d910f54127..94ba041e135 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -1,19 +1,27 @@ name: "Shellcheck" on: push: + branches: + - develop paths: - - conf/solr/** - - modules/container-base/** + - conf/solr/**/.sh + - modules/container-base/**/*.sh + - modules/container-configbaker/**/*.sh pull_request: + branches: + - develop paths: - - conf/solr/** - - modules/container-base/** + - conf/solr/**/*.sh + - modules/container-base/**/*.sh + - modules/container-configbaker/**/*.sh jobs: shellcheck: name: Shellcheck runs-on: ubuntu-latest + permissions: + pull-requests: write steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: shellcheck uses: reviewdog/action-shellcheck@v1 with: @@ -21,4 +29,19 @@ jobs: reporter: github-pr-review # Change reporter. fail_on_error: true # Container base image uses dumb-init shebang, so nail to using bash - shellcheck_flags: "--shell=bash --external-sources" \ No newline at end of file + shellcheck_flags: "--shell=bash --external-sources" + # Exclude old scripts + exclude: | + */.git/* + conf/docker-aio/* + doc/* + downloads/* + scripts/database/* + scripts/globalid/* + scripts/icons/* + scripts/installer/* + scripts/issues/* + scripts/r/* + scripts/tests/* + scripts/vagrant/* + tests/* diff --git a/.github/workflows/spi_release.yml b/.github/workflows/spi_release.yml new file mode 100644 index 00000000000..1fbf05ce693 --- /dev/null +++ b/.github/workflows/spi_release.yml @@ -0,0 +1,94 @@ +name: Dataverse SPI + +on: + push: + branch: + - "develop" + paths: + - "modules/dataverse-spi/**" + pull_request: + branch: + - "develop" + paths: + - "modules/dataverse-spi/**" + +jobs: + # Note: Pushing packages to Maven Central requires access to secrets, which pull requests from remote forks + # don't have. Skip in these cases. + check-secrets: + name: Check for Secrets Availability + runs-on: ubuntu-latest + outputs: + available: ${{ steps.secret-check.outputs.available }} + steps: + - id: secret-check + # perform secret check & put boolean result as an output + shell: bash + run: | + if [ "${{ secrets.DATAVERSEBOT_SONATYPE_USERNAME }}" != '' ]; then + echo "available=true" >> $GITHUB_OUTPUT; + else + echo "available=false" >> $GITHUB_OUTPUT; + fi + + snapshot: + name: Release Snapshot + needs: check-secrets + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' && needs.check-secrets.outputs.available == 'true' + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'adopt' + server-id: ossrh + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + - uses: actions/cache@v2 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + + - name: Deploy Snapshot + run: mvn -f modules/dataverse-spi -Dproject.version.suffix="-PR${{ github.event.number }}-SNAPSHOT" deploy + env: + MAVEN_USERNAME: ${{ secrets.DATAVERSEBOT_SONATYPE_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.DATAVERSEBOT_SONATYPE_TOKEN }} + + release: + name: Release + needs: check-secrets + runs-on: ubuntu-latest + if: github.event_name == 'push' && needs.check-secrets.outputs.available == 'true' + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'adopt' + - uses: actions/cache@v2 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + + # Running setup-java again overwrites the settings.xml - IT'S MANDATORY TO DO THIS SECOND SETUP!!! + - name: Set up Maven Central Repository + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'adopt' + server-id: ossrh + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + gpg-private-key: ${{ secrets.DATAVERSEBOT_GPG_KEY }} + gpg-passphrase: MAVEN_GPG_PASSPHRASE + + - name: Sign + Publish Release + run: mvn -f modules/dataverse-spi -P release deploy + env: + MAVEN_USERNAME: ${{ secrets.DATAVERSEBOT_SONATYPE_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.DATAVERSEBOT_SONATYPE_TOKEN }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.DATAVERSEBOT_GPG_PASSWORD }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 83671abf43e..d38538fc364 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,6 @@ src/main/webapp/resources/images/dataverseproject.png.thumb140 # apache-maven is downloaded by docker-aio apache-maven* + +# Docker development volumes +/docker-dev-volumes diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 00000000000..cadaedc1448 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,21 @@ +version: 2 + +# HTML is always built, these are additional formats only +formats: + - pdf + +build: + os: ubuntu-22.04 + tools: + python: "3.10" + apt_packages: + - graphviz + +python: + install: + - requirements: doc/sphinx-guides/requirements.txt + + +sphinx: + configuration: doc/sphinx-guides/source/conf.py + fail_on_warning: true diff --git a/conf/docker-aio/readme.md b/conf/docker-aio/readme.md index ef4d3626cf0..f3031a5bb6e 100644 --- a/conf/docker-aio/readme.md +++ b/conf/docker-aio/readme.md @@ -1,5 +1,9 @@ # Docker All-In-One +> :information_source: **NOTE: Sunsetting of this module is imminent.** There is no schedule yet, but expect it to go away. +> Please let the [Dataverse Containerization Working Group](https://ct.gdcc.io) know if you are a user and +> what should be preserved. + First pass docker all-in-one image, intended for running integration tests against. Also usable for normal development and system evaluation; not intended for production. diff --git a/conf/keycloak/oidc-keycloak-auth-provider.json b/conf/keycloak/oidc-keycloak-auth-provider.json index bc70640212d..7d09fe5f36e 100644 --- a/conf/keycloak/oidc-keycloak-auth-provider.json +++ b/conf/keycloak/oidc-keycloak-auth-provider.json @@ -3,6 +3,6 @@ "factoryAlias": "oidc", "title": "OIDC-Keycloak", "subtitle": "OIDC-Keycloak", - "factoryData": "type: oidc | issuer: http://localhost:8090/auth/realms/oidc-realm | clientId: oidc-client | clientSecret: ss6gE8mODCDfqesQaSG3gwUwZqZt547E", + "factoryData": "type: oidc | issuer: http://keycloak.mydomain.com:8090/realms/oidc-realm | clientId: oidc-client | clientSecret: ss6gE8mODCDfqesQaSG3gwUwZqZt547E", "enabled": true } diff --git a/conf/solr/8.11.1/schema.xml b/conf/solr/8.11.1/schema.xml index f11938621fc..ceff082f418 100644 --- a/conf/solr/8.11.1/schema.xml +++ b/conf/solr/8.11.1/schema.xml @@ -233,6 +233,9 @@ + + + + + + ${docker.platforms} ${project.build.directory}/buildx-state diff --git a/modules/container-base/src/main/docker/Dockerfile b/modules/container-base/src/main/docker/Dockerfile index 07968e92359..bbd02a14328 100644 --- a/modules/container-base/src/main/docker/Dockerfile +++ b/modules/container-base/src/main/docker/Dockerfile @@ -190,6 +190,9 @@ RUN < + + + + modules/container-configbaker/scripts + scripts + + + + conf/solr/8.11.1 + solr + + + + scripts/api + setup + + setup-all.sh + setup-builtin-roles.sh + setup-datasetfields.sh + setup-identity-providers.sh + + data/licenses/*.json + data/authentication-providers/builtin.json + data/metadatablocks/*.tsv + + data/dv-root.json + + data/role-admin.json + data/role-curator.json + data/role-dsContributor.json + data/role-dvContributor.json + data/role-editor.json + data/role-filedownloader.json + data/role-fullContributor.json + data/role-member.json + + data/user-admin.json + + + data/metadatablocks/custom* + + + + \ No newline at end of file diff --git a/modules/container-configbaker/scripts/bootstrap.sh b/modules/container-configbaker/scripts/bootstrap.sh new file mode 100644 index 00000000000..1aa9e232953 --- /dev/null +++ b/modules/container-configbaker/scripts/bootstrap.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +# [INFO]: Execute bootstrapping configuration of a freshly baked instance + +set -euo pipefail + +function usage() { + echo "Usage: $(basename "$0") [-h] [-u instanceUrl] [-t timeout] []" + echo "" + echo "Execute initial configuration (bootstrapping) of an empty Dataverse instance." + echo -n "Known personas: " + find "${BOOTSTRAP_DIR}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | paste -sd ' ' + echo "" + echo "Parameters:" + echo "instanceUrl - Location on container network where to reach your instance. Default: 'http://dataverse:8080'" + echo " timeout - Provide how long to wait for the instance to become available (using wait4x). Default: '2m'" + echo " persona - Configure persona to execute. Calls ${BOOTSTRAP_DIR}//init.sh. Default: 'base'" + echo "" + echo "Note: This script will wait for the Dataverse instance to be available before executing the bootstrapping." + echo " It also checks if already bootstrapped before (availability of metadata blocks) and skip if true." + echo "" + exit 1 +} + +# Set some defaults as documented +DATAVERSE_URL=${DATAVERSE_URL:-"http://dataverse:8080"} +TIMEOUT=${TIMEOUT:-"2m"} + +while getopts "u:t:h" OPTION +do + case "$OPTION" in + u) DATAVERSE_URL="$OPTARG" ;; + t) TIMEOUT="$OPTARG" ;; + h) usage;; + \?) usage;; + esac +done +shift $((OPTIND-1)) + +# Assign persona if present or go default +PERSONA=${1:-"base"} + +# Export the URL to be reused in the actual setup scripts +export DATAVERSE_URL + +# Wait for the instance to become available +echo "Waiting for ${DATAVERSE_URL} to become ready in max ${TIMEOUT}." +wait4x http "${DATAVERSE_URL}/api/info/version" -i 8s -t "$TIMEOUT" --expect-status-code 200 --expect-body-json data.version + +# Avoid bootstrapping again by checking if a metadata block has been loaded +BLOCK_COUNT=$(curl -sSf "${DATAVERSE_URL}/api/metadatablocks" | jq ".data | length") +if [[ $BLOCK_COUNT -gt 0 ]]; then + echo "Your instance has been bootstrapped already, skipping." + exit 0 +fi + +# Now execute the bootstrapping script +echo "Now executing bootstrapping script at ${BOOTSTRAP_DIR}/${PERSONA}/init.sh." +exec "${BOOTSTRAP_DIR}/${PERSONA}/init.sh" diff --git a/modules/container-configbaker/scripts/bootstrap/base/init.sh b/modules/container-configbaker/scripts/bootstrap/base/init.sh new file mode 100644 index 00000000000..81c2b59f347 --- /dev/null +++ b/modules/container-configbaker/scripts/bootstrap/base/init.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -euo pipefail + +# Set some defaults as documented +DATAVERSE_URL=${DATAVERSE_URL:-"http://dataverse:8080"} +export DATAVERSE_URL + +./setup-all.sh diff --git a/modules/container-configbaker/scripts/bootstrap/dev/init.sh b/modules/container-configbaker/scripts/bootstrap/dev/init.sh new file mode 100644 index 00000000000..1042478963d --- /dev/null +++ b/modules/container-configbaker/scripts/bootstrap/dev/init.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +set -euo pipefail + +# Set some defaults as documented +DATAVERSE_URL=${DATAVERSE_URL:-"http://dataverse:8080"} +export DATAVERSE_URL + +echo "Running base setup-all.sh (INSECURE MODE)..." +"${BOOTSTRAP_DIR}"/base/setup-all.sh --insecure -p=admin1 | tee /tmp/setup-all.sh.out + +echo "Setting system mail address..." +curl -X PUT -d "dataverse@localhost" "${DATAVERSE_URL}/api/admin/settings/:SystemEmail" + +echo "Setting DOI provider to \"FAKE\"..." +curl "${DATAVERSE_URL}/api/admin/settings/:DoiProvider" -X PUT -d FAKE + +API_TOKEN=$(grep apiToken "/tmp/setup-all.sh.out" | jq ".data.apiToken" | tr -d \") +export API_TOKEN + +echo "Publishing root dataverse..." +curl -H "X-Dataverse-key:$API_TOKEN" -X POST "${DATAVERSE_URL}/api/dataverses/:root/actions/:publish" + +echo "Allowing users to create dataverses and datasets in root..." +curl -H "X-Dataverse-key:$API_TOKEN" -X POST -H "Content-type:application/json" -d "{\"assignee\": \":authenticated-users\",\"role\": \"fullContributor\"}" "${DATAVERSE_URL}/api/dataverses/:root/assignments" + +echo "Checking Dataverse version..." +curl "${DATAVERSE_URL}/api/info/version" + +echo "" +echo "Done, your instance has been configured for development. Have a nice day!" diff --git a/modules/container-configbaker/scripts/fix-fs-perms.sh b/modules/container-configbaker/scripts/fix-fs-perms.sh new file mode 100644 index 00000000000..9ce8f475d70 --- /dev/null +++ b/modules/container-configbaker/scripts/fix-fs-perms.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# [INFO]: Fix folder permissions using 'chown' to be writeable by containers not running as root. + +set -euo pipefail + +if [[ "$(id -un)" != "root" ]]; then + echo "This script must be run as user root (not $(id -un)), otherwise no fix is possible." +fi + +DEF_DV_PATH="/dv" +DEF_SOLR_PATH="/var/solr" +DEF_DV_UID="1000" +DEF_SOLR_UID="8983" + +function usage() { + echo "Usage: $(basename "$0") (dv|solr|[1-9][0-9]{3,4}) [PATH [PATH [...]]]" + echo "" + echo "You may omit a path when using 'dv' or 'solr' as first argument:" + echo " - 'dv' will default to user $DEF_DV_UID and $DEF_DV_PATH" + echo " - 'solr' will default to user $DEF_SOLR_UID and $DEF_SOLR_PATH" + exit 1 +} + +# Get a target name or id +TARGET=${1:-help} +# Get the rest of the arguments as paths to apply the fix to +PATHS=( "${@:2}" ) + +ID=0 +case "$TARGET" in + dv) + ID="$DEF_DV_UID" + # If there is no path, add the default for our app image + if [[ ${#PATHS[@]} -eq 0 ]]; then + PATHS=( "$DEF_DV_PATH" ) + fi + ;; + solr) + ID="$DEF_SOLR_UID" + # In case there is no path, add the default path for Solr images + if [[ ${#PATHS[@]} -eq 0 ]]; then + PATHS=( "$DEF_SOLR_PATH" ) + fi + ;; + # If there is a digit in the argument, check if this is a valid UID (>= 1000, ...) + *[[:digit:]]* ) + echo "$TARGET" | grep -q "^[1-9][0-9]\{3,4\}$" || usage + ID="$TARGET" + ;; + *) + usage + ;; +esac + +# Check that we actually have at least 1 path +if [[ ${#PATHS[@]} -eq 0 ]]; then + usage +fi + +# Do what we came for +chown -R "$ID:$ID" "${PATHS[@]}" diff --git a/modules/container-configbaker/scripts/help.sh b/modules/container-configbaker/scripts/help.sh new file mode 100644 index 00000000000..744ec8c8b4c --- /dev/null +++ b/modules/container-configbaker/scripts/help.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -euo pipefail + +# [INFO]: This script. + +# This is the Dataverse logo in ASCII +# shellcheck disable=SC2016 +echo -e ' â•“mαo\n â•« jh\n `%╥æ╨\n ╫µ\n â•“@M%â•—,\n â–“` â•«U\n ▓² â•«â•›\n â–“M#Mâ•"\n ڑMâ•â•%φ╫┘\n┌╫" "â•«â”\nâ–“ â–“\nâ–“ â–“\n`╫µ ¿╫"\n "â•œ%%MMâ•œ`' +echo "" +echo "Hello!" +echo "" +echo "My name is Config Baker. I'm a container image with lots of tooling to 'bake' a containerized Dataverse instance!" +echo "I can cook up an instance (initial config), put icing on your Solr search index configuration, and more!" +echo "" +echo "Here's a list of things I can do for you:" + +# Get the longest name length +LENGTH=1 +for SCRIPT in "${SCRIPT_DIR}"/*.sh; do + L="$(basename "$SCRIPT" | wc -m)" + if [ "$L" -gt "$LENGTH" ]; then + LENGTH="$L" + fi +done + +# Print script names and info, but formatted +for SCRIPT in "${SCRIPT_DIR}"/*.sh; do + printf "%${LENGTH}s - " "$(basename "$SCRIPT")" + grep "# \[INFO\]: " "$SCRIPT" | sed -e "s|# \[INFO\]: ||" +done + +echo "" +echo "Simply execute this container with the script name (and potentially arguments) as 'command'." diff --git a/modules/dataverse-parent/pom.xml b/modules/dataverse-parent/pom.xml index d85d8aed5a1..05f7874d31c 100644 --- a/modules/dataverse-parent/pom.xml +++ b/modules/dataverse-parent/pom.xml @@ -14,6 +14,7 @@ ../../pom.xml ../../scripts/zipdownload ../container-base + ../dataverse-spi - 5.13 + 5.14 11 UTF-8 @@ -186,10 +187,19 @@ 3.0.0-M5 3.0.0-M5 3.3.0 + 3.0.0-M7 + 3.0.1 + 4.0.0-M4 + 3.2.1 + 3.4.1 + 1.3.0 + 3.1.2 + 1.6.13 + 1.7.0 - 0.40.2 + 0.43.0 @@ -262,6 +272,46 @@ docker-maven-plugin ${fabric8-dmp.version} + + org.apache.maven.plugins + maven-site-plugin + ${maven-site-plugin.version} + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + org.codehaus.mojo + flatten-maven-plugin + ${maven-flatten-plugin.version} + + + org.kordamp.maven + pomchecker-maven-plugin + ${pomchecker-maven-plugin.version} + + + org.sonatype.plugins + nexus-staging-maven-plugin + ${nexus-staging-plugin.version} + + + org.apache.maven.plugins + maven-release-plugin + ${maven-release-plugin.version} + @@ -345,8 +395,9 @@ - 5.2022.4 + 5.2022.5 diff --git a/modules/dataverse-spi/.gitignore b/modules/dataverse-spi/.gitignore new file mode 100644 index 00000000000..d75620abf70 --- /dev/null +++ b/modules/dataverse-spi/.gitignore @@ -0,0 +1 @@ +.flattened-pom.xml diff --git a/modules/dataverse-spi/pom.xml b/modules/dataverse-spi/pom.xml new file mode 100644 index 00000000000..6235d309e89 --- /dev/null +++ b/modules/dataverse-spi/pom.xml @@ -0,0 +1,238 @@ + + + 4.0.0 + + + edu.harvard.iq + dataverse-parent + ${revision} + ../dataverse-parent + + + io.gdcc + dataverse-spi + 1.0.0${project.version.suffix} + jar + + Dataverse SPI Plugin API + https://dataverse.org + + A package to create out-of-tree Java code for Dataverse Software. Plugin projects can use this package + as an API dependency just like Jakarta EE APIs if they want to create external plugins. These will be loaded + at runtime of a Dataverse installation using SPI. See also https://guides.dataverse.org/en/latest/developers + for more information. + + + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + Dataverse Core Team + support@dataverse.org + + + + + https://github.com/IQSS/dataverse/issues + GitHub Issues + + + + scm:git:git@github.com:IQSS/dataverse.git + scm:git:git@github.com:IQSS/dataverse.git + git@github.com:IQSS/dataverse.git + HEAD + + + + https://github.com/IQSS/dataverse/actions + github + + +
dataversebot@gdcc.io
+
+
+
+ + + + ossrh + https://s01.oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + + none + false + + + + + jakarta.json + jakarta.json-api + provided + + + + jakarta.ws.rs + jakarta.ws.rs-api + provided + + + + + + + + maven-compiler-plugin + + ${target.java.version} + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + true + + ossrh + https://s01.oss.sonatype.org + true + + + + org.apache.maven.plugins + maven-release-plugin + + false + release + true + deploy + + + + org.codehaus.mojo + flatten-maven-plugin + + true + oss + + remove + remove + + + + + + flatten + process-resources + + flatten + + + + + flatten.clean + clean + + clean + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + ${skipDeploy} + + + + + + + + release + + + + org.apache.maven.plugins + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + org.kordamp.maven + pomchecker-maven-plugin + + + process-resources + + check-maven-central + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + ${target.java.version} + false + ${javadoc.lint} + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + + + + ct + + true + + + +
diff --git a/modules/dataverse-spi/src/main/java/io/gdcc/spi/export/ExportDataProvider.java b/modules/dataverse-spi/src/main/java/io/gdcc/spi/export/ExportDataProvider.java new file mode 100644 index 00000000000..228992c8288 --- /dev/null +++ b/modules/dataverse-spi/src/main/java/io/gdcc/spi/export/ExportDataProvider.java @@ -0,0 +1,96 @@ +package io.gdcc.spi.export; + +import java.io.InputStream; +import java.util.Optional; + +import javax.json.JsonArray; +import javax.json.JsonObject; + +/** + * Provides all the metadata Dataverse has about a given dataset that can then + * be used by an @see Exporter to create a new metadata export format. + * + */ +public interface ExportDataProvider { + + /** + * @return - dataset metadata in the standard Dataverse JSON format used in the + * API and available as the JSON metadata export via the user interface. + * @apiNote - there is no JSON schema defining this output, but the format is + * well documented in the Dataverse online guides. This, and the + * OAI_ORE export are the only two that provide 'complete' + * dataset-level metadata along with basic file metadata for each file + * in the dataset. + */ + JsonObject getDatasetJson(); + + /** + * + * @return - dataset metadata in the JSON-LD based OAI_ORE format used in + * Dataverse's archival bag export mechanism and as available in the + * user interface and by API. + * @apiNote - THis, and the JSON format are the only two that provide complete + * dataset-level metadata along with basic file metadata for each file + * in the dataset. + */ + JsonObject getDatasetORE(); + + /** + * Dataverse is capable of extracting DDI-centric metadata from tabular + * datafiles. This detailed metadata, which is only available for successfully + * "ingested" tabular files, is not included in the output of any other methods + * in this interface. + * + * @return - a JSONArray with one entry per ingested tabular dataset file. + * @apiNote - there is no JSON schema available for this output and the format + * is not well documented. Implementers may wish to expore the @see + * edu.harvard.iq.dataverse.export.DDIExporter and the @see + * edu.harvard.iq.dataverse.util.json.JSONPrinter classes where this + * output is used/generated (respectively). + */ + JsonArray getDatasetFileDetails(); + + /** + * + * @return - the subset of metadata conforming to the schema.org standard as + * available in the user interface and as included as header metadata in + * dataset pages (for use by search engines) + * @apiNote - as this metadata export is not complete, it should only be used as + * a starting point for an Exporter if it simplifies your exporter + * relative to using the JSON or OAI_ORE exports. + */ + JsonObject getDatasetSchemaDotOrg(); + + /** + * + * @return - the subset of metadata conforming to the DataCite standard as + * available in the Dataverse user interface and as sent to DataCite when DataCite DOIs are used. + * @apiNote - as this metadata export is not complete, it should only be used as + * a starting point for an Exporter if it simplifies your exporter + * relative to using the JSON or OAI_ORE exports. + */ + String getDataCiteXml(); + + /** + * If an Exporter has specified a prerequisite format name via the + * getPrerequisiteFormatName() method, it can call this method to retrieve + * metadata in that format. + * + * @return - metadata in the specified prerequisite format (if available from + * another internal or added Exporter) as an Optional + * @apiNote - This functionality is intended as way to easily generate alternate + * formats of the ~same metadata, e.g. to support download as XML, + * HTML, PDF for a specific metadata standard (e.g. DDI). It can be + * particularly useful, reative to starting from the output of one of + * the getDataset* methods above, if there are existing libraries that + * can convert between these formats. Note that, since Exporters can be + * replaced, relying on this method could cause your Exporter to + * malfunction, e.g. if you depend on format "ddi" and a third party + * Exporter is configured to replace the internal ddi Exporter in + * Dataverse. + */ + default Optional getPrerequisiteInputStream() { + return Optional.empty(); + } + +} diff --git a/modules/dataverse-spi/src/main/java/io/gdcc/spi/export/ExportException.java b/modules/dataverse-spi/src/main/java/io/gdcc/spi/export/ExportException.java new file mode 100644 index 00000000000..c816a605860 --- /dev/null +++ b/modules/dataverse-spi/src/main/java/io/gdcc/spi/export/ExportException.java @@ -0,0 +1,13 @@ +package io.gdcc.spi.export; + +import java.io.IOException; + +public class ExportException extends IOException { + public ExportException(String message) { + super(message); + } + + public ExportException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/modules/dataverse-spi/src/main/java/io/gdcc/spi/export/Exporter.java b/modules/dataverse-spi/src/main/java/io/gdcc/spi/export/Exporter.java new file mode 100644 index 00000000000..1338a3c9734 --- /dev/null +++ b/modules/dataverse-spi/src/main/java/io/gdcc/spi/export/Exporter.java @@ -0,0 +1,110 @@ +package io.gdcc.spi.export; + +import java.io.OutputStream; +import java.util.Locale; +import java.util.Optional; + + +/** + * Dataverse allows new metadata export formats to be dynamically added a running instance. This is done by + * deploying new classes that implement this Exporter interface. + */ + +public interface Exporter { + + + /** + * When this method is called, the Exporter should write the metadata to the given OutputStream. + * + * @apiNote When implementing exportDataset, when done writing content, please make sure + * to flush() the outputStream, but NOT close() it! This way an exporter can be + * used to insert the produced metadata into the body of an HTTP response, etc. + * (for example, to insert it into the body of an OAI response, where more XML + * needs to be written, for the outer OAI-PMH record). -- L.A. 4.5 + * + * @param dataProvider - the @see ExportDataProvider interface includes several methods that can be used to retrieve the dataset metadata in different formats. An Exporter should use one or more of these to obtain the values needed to generate metadata in the format it supports. + * @param outputStream - the OutputStream to write the metadata to + * @throws ExportException - if there is an error writing the metadata + */ + void exportDataset(ExportDataProvider dataProvider, OutputStream outputStream) throws ExportException; + + /** + * This method should return the name of the metadata format this Exporter + * provides. + * + * @apiNote Format names are unique identifiers for the formats supported in + * Dataverse. Reusing the same format name as another Exporter will + * result only one implementation being available. Exporters packaged + * as an external Jar file have precedence over the default + * implementations in Dataverse. Hence re-using one of the existing + * format names will result in the Exporter replacing the internal one + * with the same name. The precedence between two external Exporters + * using the same format name is not defined. + * Current format names used internally by Dataverse are: + * Datacite + * dcterms + * ddi + * oai_dc + * html + * dataverse_json + * oai_ddi + * OAI_ORE + * oai_datacite + * schema.org + * + * @return - the unique name of the metadata format this Exporter + */ + String getFormatName(); + + /** + * This method should return the display name of the metadata format this + * Exporter provides. Display names are used in the UI, specifically in the menu + * of avaiable Metadata Exports on the dataset page/metadata tab to identify the + * format. + */ + String getDisplayName(Locale locale); + + /** + * Exporters can specify that they require, as input, the output of another + * exporter. This is done by providing the name of that format in response to a + * call to this method. + * + * @implNote The one current example where this is done is with the html(display + * name "DDI html codebook") exporter which starts from the XML-based + * ddi format produced by that exporter. + * @apiNote - The Exporter can expect that the metadata produced by its + * prerequisite exporter (as defined with this method) will be + * available via the ExportDataProvider.getPrerequisiteInputStream() + * method. The default implementation of this method returns an empty + * value which means the getPrerequisiteInputStream() method of the + * ExportDataProvider sent in the exportDataset method will return an + * empty Optional. + * + */ + default Optional getPrerequisiteFormatName() { + return Optional.empty(); + } + + + /** + * Harvestable Exporters will be available as options in Dataverse's Harvesting mechanism. + * @return true to make this exporter available as a harvesting option. + */ + Boolean isHarvestable(); + + /** + * If an Exporter is available to users, its format will be generated for every + * published dataset and made available via the dataset page/metadata + * tab/Metadata Exports menu item and via the API. + * @return true to make this exporter available to users. + */ + Boolean isAvailableToUsers(); + + /** + * To support effective downloads of metadata in this Exporter's format, the Exporter should specify an appropriate mime type. + * @apiNote - It is recommended to used the @see javax.ws.rs.core.MediaType enum to specify the mime type. + * @return The mime type, e.g. "application/json", "text/plain", etc. + */ + String getMediaType(); + +} diff --git a/modules/dataverse-spi/src/main/java/io/gdcc/spi/export/XMLExporter.java b/modules/dataverse-spi/src/main/java/io/gdcc/spi/export/XMLExporter.java new file mode 100644 index 00000000000..9afe7ba1cfd --- /dev/null +++ b/modules/dataverse-spi/src/main/java/io/gdcc/spi/export/XMLExporter.java @@ -0,0 +1,37 @@ +package io.gdcc.spi.export; + +import javax.ws.rs.core.MediaType; + +/** + * XML Exporter is an extension of the base Exporter interface that adds the + * additional methods needed for generating XML metadata export formats. + */ +public interface XMLExporter extends Exporter { + + /** + * @implNote for the ddi exporter, this method returns "ddi:codebook:2_5" + * @return - the name space of the XML schema + */ + String getXMLNameSpace(); + + /** + * @apiNote According to the XML specification, the value must be a URI + * @implNote for the ddi exporter, this method returns + * "https://ddialliance.org/Specification/DDI-Codebook/2.5/XMLSchema/codebook.xsd" + * @return - the location of the XML schema as a String (must be a valid URI) + */ + String getXMLSchemaLocation(); + + /** + * @implNote for the ddi exporter, this method returns "2.5" + * @return - the version of the XML schema + */ + String getXMLSchemaVersion(); + + /** + * @return - should always be MediaType.APPLICATION_XML + */ + public default String getMediaType() { + return MediaType.APPLICATION_XML; + }; +} diff --git a/pom.xml b/pom.xml index 8b6f98c5896..96f598af0f5 100644 --- a/pom.xml +++ b/pom.xml @@ -15,10 +15,16 @@ doc/sphinx-guides/source/developers/dependencies.rst --> dataverse - war + ${packaging.type} dataverse false + false + + + + war + 1.2.18.4 8.5.10 1.20.1 @@ -63,7 +69,7 @@ runtime - + org.passay passay 1.6.0 @@ -178,6 +184,11 @@ provided + + fish.payara.api + payara-api + provided + com.sun.mail jakarta.mail @@ -381,7 +392,7 @@ com.nimbusds oauth2-oidc-sdk - 9.41.1 + 10.7.1 @@ -499,7 +510,11 @@ cdm-core ${netcdf.version} - + + io.gdcc + dataverse-spi + 1.0.0 + org.junit.jupiter @@ -754,22 +769,128 @@ - tc + ct + true - 9.6 + true + + docker-build + 13 + + gdcc/dataverse:${app.image.tag} + unstable + gdcc/base:${base.image.tag} + unstable + gdcc/configbaker:${conf.image.tag} + ${app.image.tag} + + + + + ${app.image} + ${postgresql.server.version} + ${solr.version} + dataverse + + + + org.apache.maven.plugins + maven-war-plugin + + + prepare-package + + exploded + + + + + + + + + + io.fabric8 + docker-maven-plugin + true + + + + + dev_dataverse + ${app.image} + + + + ${docker.platforms} + + + Dockerfile + + ${base.image} + + @ + + assembly.xml + + + + + + + + compose + ${project.basedir} + docker-compose-dev.yml + + + + + dev_bootstrap + ${conf.image} + + + + ${docker.platforms} + + + ${project.basedir}/modules/container-configbaker/Dockerfile + + ${SOLR_VERSION} + + @ + + ${project.basedir}/modules/container-configbaker/assembly.xml + + + + + + true + + + + true + + org.apache.maven.plugins maven-failsafe-plugin ${maven-failsafe-plugin.version} - testcontainers + end2end ${postgresql.server.version} + ${skipIntegrationTests} diff --git a/scripts/api/data/dataset-create-new-all-default-fields.json b/scripts/api/data/dataset-create-new-all-default-fields.json index d7ae8cefbf7..4af128955c9 100644 --- a/scripts/api/data/dataset-create-new-all-default-fields.json +++ b/scripts/api/data/dataset-create-new-all-default-fields.json @@ -466,9 +466,9 @@ }, { "typeName": "productionPlace", - "multiple": false, + "multiple": true, "typeClass": "primitive", - "value": "ProductionPlace" + "value": ["ProductionPlace"] }, { "typeName": "contributor", @@ -710,9 +710,9 @@ }, { "typeName": "series", - "multiple": false, + "multiple": true, "typeClass": "compound", - "value": { + "value": [{ "seriesName": { "typeName": "seriesName", "multiple": false, @@ -725,7 +725,7 @@ "typeClass": "primitive", "value": "SeriesInformation" } - } + }] }, { "typeName": "software", @@ -899,25 +899,25 @@ "typeName": "westLongitude", "multiple": false, "typeClass": "primitive", - "value": "10" + "value": "-72" }, "eastLongitude": { "typeName": "eastLongitude", "multiple": false, "typeClass": "primitive", - "value": "20" + "value": "-70" }, "northLongitude": { "typeName": "northLongitude", "multiple": false, "typeClass": "primitive", - "value": "30" + "value": "43" }, "southLongitude": { "typeName": "southLongitude", "multiple": false, "typeClass": "primitive", - "value": "40" + "value": "42" } }, { @@ -925,25 +925,25 @@ "typeName": "westLongitude", "multiple": false, "typeClass": "primitive", - "value": "50" + "value": "-18" }, "eastLongitude": { "typeName": "eastLongitude", "multiple": false, "typeClass": "primitive", - "value": "60" + "value": "-13" }, "northLongitude": { "typeName": "northLongitude", "multiple": false, "typeClass": "primitive", - "value": "70" + "value": "29" }, "southLongitude": { "typeName": "southLongitude", "multiple": false, "typeClass": "primitive", - "value": "80" + "value": "28" } } ] @@ -1404,7 +1404,7 @@ "multiple": true, "typeClass": "controlledVocabulary", "value": [ - "cell counting", + "genome sequencing", "cell sorting", "clinical chemistry analysis", "DNA methylation profiling" diff --git a/scripts/api/data/dataset-create-new.json b/scripts/api/data/dataset-create-new.json index 0017da15974..5831e0b17e6 100644 --- a/scripts/api/data/dataset-create-new.json +++ b/scripts/api/data/dataset-create-new.json @@ -4,6 +4,10 @@ "persistentUrl": "http://dx.doi.org/10.5072/FK2/9", "protocol": "chadham-house-rule", "datasetVersion": { + "license": { + "name": "CC0 1.0", + "uri": "http://creativecommons.org/publicdomain/zero/1.0" + }, "metadataBlocks": { "citation": { "displayName": "Citation Metadata", @@ -121,4 +125,4 @@ } } } -} \ No newline at end of file +} diff --git a/scripts/api/data/dataset-finch1_fr.json b/scripts/api/data/dataset-finch1_fr.json index ce9616fdef5..848e5e3587e 100644 --- a/scripts/api/data/dataset-finch1_fr.json +++ b/scripts/api/data/dataset-finch1_fr.json @@ -1,6 +1,10 @@ { "metadataLanguage": "fr", "datasetVersion": { + "license": { + "name": "CC0 1.0", + "uri": "http://creativecommons.org/publicdomain/zero/1.0" + }, "metadataBlocks": { "citation": { "fields": [ diff --git a/scripts/api/data/metadatablocks/citation.tsv b/scripts/api/data/metadatablocks/citation.tsv index be32bb7134e..18bc31c2dd6 100644 --- a/scripts/api/data/metadatablocks/citation.tsv +++ b/scripts/api/data/metadatablocks/citation.tsv @@ -66,7 +66,7 @@ dateOfCollectionStart Start Date The date when the data collection started YYYY-MM-DD date 62 #NAME: #VALUE FALSE FALSE FALSE FALSE FALSE FALSE dateOfCollection citation dateOfCollectionEnd End Date The date when the data collection ended YYYY-MM-DD date 63 #NAME: #VALUE FALSE FALSE FALSE FALSE FALSE FALSE dateOfCollection citation kindOfData Data Type The type of data included in the files (e.g. survey data, clinical data, or machine-readable text) text 64 TRUE FALSE TRUE TRUE FALSE FALSE citation http://rdf-vocabulary.ddialliance.org/discovery#kindOfData - series Series Information about the dataset series to which the Dataset belong none 65 : FALSE FALSE FALSE FALSE FALSE FALSE citation + series Series Information about the dataset series to which the Dataset belong none 65 : FALSE FALSE TRUE FALSE FALSE FALSE citation seriesName Name The name of the dataset series text 66 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE series citation seriesInformation Information Can include 1) a history of the series and 2) a summary of features that apply to the series textbox 67 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE series citation software Software Information about the software used to generate the Dataset none 68 , FALSE FALSE TRUE FALSE FALSE FALSE citation https://www.w3.org/TR/prov-o/#wasGeneratedBy diff --git a/scripts/api/setup-all.sh b/scripts/api/setup-all.sh index c4bd6c2c9c5..e247caa72b5 100755 --- a/scripts/api/setup-all.sh +++ b/scripts/api/setup-all.sh @@ -3,7 +3,14 @@ SECURESETUP=1 DV_SU_PASSWORD="admin" -for opt in $* +DATAVERSE_URL=${DATAVERSE_URL:-"http://localhost:8080"} +# Make sure scripts we call from this one also get this env var! +export DATAVERSE_URL + +# scripts/api when called from the root of the source tree +SCRIPT_PATH="$(dirname "$0")" + +for opt in "$@" do case $opt in "--insecure") @@ -24,13 +31,9 @@ do esac done +# shellcheck disable=SC2016 command -v jq >/dev/null 2>&1 || { echo >&2 '`jq` ("sed for JSON") is required, but not installed. Download the binary for your platform from http://stedolan.github.io/jq/ and make sure it is in your $PATH (/usr/bin/jq is fine) and executable with `sudo chmod +x /usr/bin/jq`. On Mac, you can install it with `brew install jq` if you use homebrew: http://brew.sh . Aborting.'; exit 1; } -echo "deleting all data from Solr" -curl http://localhost:8983/solr/collection1/update/json?commit=true -H "Content-type: application/json" -X POST -d "{\"delete\": { \"query\":\"*:*\"}}" - -SERVER=http://localhost:8080/api - # Everything + the kitchen sink, in a single script # - Setup the metadata blocks and controlled vocabulary # - Setup the builtin roles @@ -41,49 +44,49 @@ SERVER=http://localhost:8080/api echo "Setup the metadata blocks" -./setup-datasetfields.sh +"$SCRIPT_PATH"/setup-datasetfields.sh echo "Setup the builtin roles" -./setup-builtin-roles.sh +"$SCRIPT_PATH"/setup-builtin-roles.sh echo "Setup the authentication providers" -./setup-identity-providers.sh +"$SCRIPT_PATH"/setup-identity-providers.sh echo "Setting up the settings" echo "- Allow internal signup" -curl -X PUT -d yes "$SERVER/admin/settings/:AllowSignUp" -curl -X PUT -d /dataverseuser.xhtml?editMode=CREATE "$SERVER/admin/settings/:SignUpUrl" - -curl -X PUT -d doi "$SERVER/admin/settings/:Protocol" -curl -X PUT -d 10.5072 "$SERVER/admin/settings/:Authority" -curl -X PUT -d "FK2/" "$SERVER/admin/settings/:Shoulder" -curl -X PUT -d DataCite "$SERVER/admin/settings/:DoiProvider" -curl -X PUT -d burrito $SERVER/admin/settings/BuiltinUsers.KEY -curl -X PUT -d localhost-only $SERVER/admin/settings/:BlockedApiPolicy -curl -X PUT -d 'native/http' $SERVER/admin/settings/:UploadMethods +curl -X PUT -d yes "${DATAVERSE_URL}/api/admin/settings/:AllowSignUp" +curl -X PUT -d "/dataverseuser.xhtml?editMode=CREATE" "${DATAVERSE_URL}/api/admin/settings/:SignUpUrl" + +curl -X PUT -d doi "${DATAVERSE_URL}/api/admin/settings/:Protocol" +curl -X PUT -d 10.5072 "${DATAVERSE_URL}/api/admin/settings/:Authority" +curl -X PUT -d "FK2/" "${DATAVERSE_URL}/api/admin/settings/:Shoulder" +curl -X PUT -d DataCite "${DATAVERSE_URL}/api/admin/settings/:DoiProvider" +curl -X PUT -d burrito "${DATAVERSE_URL}/api/admin/settings/BuiltinUsers.KEY" +curl -X PUT -d localhost-only "${DATAVERSE_URL}/api/admin/settings/:BlockedApiPolicy" +curl -X PUT -d 'native/http' "${DATAVERSE_URL}/api/admin/settings/:UploadMethods" echo echo "Setting up the admin user (and as superuser)" -adminResp=$(curl -s -H "Content-type:application/json" -X POST -d @data/user-admin.json "$SERVER/builtin-users?password=$DV_SU_PASSWORD&key=burrito") -echo $adminResp -curl -X POST "$SERVER/admin/superuser/dataverseAdmin" +adminResp=$(curl -s -H "Content-type:application/json" -X POST -d @"$SCRIPT_PATH"/data/user-admin.json "${DATAVERSE_URL}/api/builtin-users?password=$DV_SU_PASSWORD&key=burrito") +echo "$adminResp" +curl -X POST "${DATAVERSE_URL}/api/admin/superuser/dataverseAdmin" echo echo "Setting up the root dataverse" -adminKey=$(echo $adminResp | jq .data.apiToken | tr -d \") -curl -s -H "Content-type:application/json" -X POST -d @data/dv-root.json "$SERVER/dataverses/?key=$adminKey" +adminKey=$(echo "$adminResp" | jq .data.apiToken | tr -d \") +curl -s -H "Content-type:application/json" -X POST -d @"$SCRIPT_PATH"/data/dv-root.json "${DATAVERSE_URL}/api/dataverses/?key=$adminKey" echo echo "Set the metadata block for Root" -curl -s -X POST -H "Content-type:application/json" -d "[\"citation\"]" $SERVER/dataverses/:root/metadatablocks/?key=$adminKey +curl -s -X POST -H "Content-type:application/json" -d "[\"citation\"]" "${DATAVERSE_URL}/api/dataverses/:root/metadatablocks/?key=$adminKey" echo echo "Set the default facets for Root" -curl -s -X POST -H "Content-type:application/json" -d "[\"authorName\",\"subject\",\"keywordValue\",\"dateOfDeposit\"]" $SERVER/dataverses/:root/facets/?key=$adminKey +curl -s -X POST -H "Content-type:application/json" -d "[\"authorName\",\"subject\",\"keywordValue\",\"dateOfDeposit\"]" "${DATAVERSE_URL}/api/dataverses/:root/facets/?key=$adminKey" echo echo "Set up licenses" # Note: CC0 has been added and set as the default license through # Flyway script V5.9.0.1__7440-configurable-license-list.sql -curl -X POST -H 'Content-Type: application/json' -H "X-Dataverse-key:$adminKey" $SERVER/licenses --upload-file data/licenses/licenseCC-BY-4.0.json +curl -X POST -H 'Content-Type: application/json' -H "X-Dataverse-key:$adminKey" "${DATAVERSE_URL}/api/licenses" --upload-file "$SCRIPT_PATH"/data/licenses/licenseCC-BY-4.0.json # OPTIONAL USERS AND DATAVERSES #./setup-optional.sh @@ -92,8 +95,8 @@ if [ $SECURESETUP = 1 ] then # Revoke the "burrito" super-key; # Block sensitive API endpoints; - curl -X DELETE $SERVER/admin/settings/BuiltinUsers.KEY - curl -X PUT -d 'admin,builtin-users' $SERVER/admin/settings/:BlockedApiEndpoints + curl -X DELETE "${DATAVERSE_URL}/api/admin/settings/BuiltinUsers.KEY" + curl -X PUT -d 'admin,builtin-users' "${DATAVERSE_URL}/api/admin/settings/:BlockedApiEndpoints" echo "Access to the /api/admin and /api/test is now disabled, except for connections from localhost." else echo "IMPORTANT!!!" diff --git a/scripts/api/setup-builtin-roles.sh b/scripts/api/setup-builtin-roles.sh index 0f3c1c150cd..f1f268debbc 100755 --- a/scripts/api/setup-builtin-roles.sh +++ b/scripts/api/setup-builtin-roles.sh @@ -1,34 +1,37 @@ -SERVER=http://localhost:8080/api +#!/bin/bash + +DATAVERSE_URL=${DATAVERSE_URL:-"http://localhost:8080"} +SCRIPT_PATH="$(dirname "$0")" # Setup the builtin roles echo "Setting up admin role" -curl -H "Content-type:application/json" -d @data/role-admin.json http://localhost:8080/api/admin/roles/ +curl -H "Content-type:application/json" -d @"$SCRIPT_PATH"/data/role-admin.json "${DATAVERSE_URL}/api/admin/roles/" echo echo "Setting up file downloader role" -curl -H "Content-type:application/json" -d @data/role-filedownloader.json http://localhost:8080/api/admin/roles/ +curl -H "Content-type:application/json" -d @"$SCRIPT_PATH"/data/role-filedownloader.json "${DATAVERSE_URL}/api/admin/roles/" echo echo "Setting up full contributor role" -curl -H "Content-type:application/json" -d @data/role-fullContributor.json http://localhost:8080/api/admin/roles/ +curl -H "Content-type:application/json" -d @"$SCRIPT_PATH"/data/role-fullContributor.json "${DATAVERSE_URL}/api/admin/roles/" echo echo "Setting up dv contributor role" -curl -H "Content-type:application/json" -d @data/role-dvContributor.json http://localhost:8080/api/admin/roles/ +curl -H "Content-type:application/json" -d @"$SCRIPT_PATH"/data/role-dvContributor.json "${DATAVERSE_URL}/api/admin/roles/" echo echo "Setting up ds contributor role" -curl -H "Content-type:application/json" -d @data/role-dsContributor.json http://localhost:8080/api/admin/roles/ +curl -H "Content-type:application/json" -d @"$SCRIPT_PATH"/data/role-dsContributor.json "${DATAVERSE_URL}/api/admin/roles/" echo echo "Setting up editor role" -curl -H "Content-type:application/json" -d @data/role-editor.json http://localhost:8080/api/admin/roles/ +curl -H "Content-type:application/json" -d @"$SCRIPT_PATH"/data/role-editor.json "${DATAVERSE_URL}/api/admin/roles/" echo echo "Setting up curator role" -curl -H "Content-type:application/json" -d @data/role-curator.json http://localhost:8080/api/admin/roles/ +curl -H "Content-type:application/json" -d @"$SCRIPT_PATH"/data/role-curator.json "${DATAVERSE_URL}/api/admin/roles/" echo echo "Setting up member role" -curl -H "Content-type:application/json" -d @data/role-member.json http://localhost:8080/api/admin/roles/ +curl -H "Content-type:application/json" -d @"$SCRIPT_PATH"/data/role-member.json "${DATAVERSE_URL}/api/admin/roles/" echo diff --git a/scripts/api/setup-datasetfields.sh b/scripts/api/setup-datasetfields.sh index 0d2d60b9538..51da677ceb8 100755 --- a/scripts/api/setup-datasetfields.sh +++ b/scripts/api/setup-datasetfields.sh @@ -1,9 +1,13 @@ -#!/bin/sh -curl http://localhost:8080/api/admin/datasetfield/loadNAControlledVocabularyValue +#!/bin/bash + +DATAVERSE_URL=${DATAVERSE_URL:-"http://localhost:8080"} +SCRIPT_PATH="$(dirname "$0")" + +curl "${DATAVERSE_URL}/api/admin/datasetfield/loadNAControlledVocabularyValue" # TODO: The "@" is confusing. Consider switching to --upload-file citation.tsv -curl http://localhost:8080/api/admin/datasetfield/load -X POST --data-binary @data/metadatablocks/citation.tsv -H "Content-type: text/tab-separated-values" -curl http://localhost:8080/api/admin/datasetfield/load -X POST --data-binary @data/metadatablocks/geospatial.tsv -H "Content-type: text/tab-separated-values" -curl http://localhost:8080/api/admin/datasetfield/load -X POST --data-binary @data/metadatablocks/social_science.tsv -H "Content-type: text/tab-separated-values" -curl http://localhost:8080/api/admin/datasetfield/load -X POST --data-binary @data/metadatablocks/astrophysics.tsv -H "Content-type: text/tab-separated-values" -curl http://localhost:8080/api/admin/datasetfield/load -X POST --data-binary @data/metadatablocks/biomedical.tsv -H "Content-type: text/tab-separated-values" -curl http://localhost:8080/api/admin/datasetfield/load -X POST --data-binary @data/metadatablocks/journals.tsv -H "Content-type: text/tab-separated-values" +curl "${DATAVERSE_URL}/api/admin/datasetfield/load" -X POST --data-binary @"$SCRIPT_PATH"/data/metadatablocks/citation.tsv -H "Content-type: text/tab-separated-values" +curl "${DATAVERSE_URL}/api/admin/datasetfield/load" -X POST --data-binary @"$SCRIPT_PATH"/data/metadatablocks/geospatial.tsv -H "Content-type: text/tab-separated-values" +curl "${DATAVERSE_URL}/api/admin/datasetfield/load" -X POST --data-binary @"$SCRIPT_PATH"/data/metadatablocks/social_science.tsv -H "Content-type: text/tab-separated-values" +curl "${DATAVERSE_URL}/api/admin/datasetfield/load" -X POST --data-binary @"$SCRIPT_PATH"/data/metadatablocks/astrophysics.tsv -H "Content-type: text/tab-separated-values" +curl "${DATAVERSE_URL}/api/admin/datasetfield/load" -X POST --data-binary @"$SCRIPT_PATH"/data/metadatablocks/biomedical.tsv -H "Content-type: text/tab-separated-values" +curl "${DATAVERSE_URL}/api/admin/datasetfield/load" -X POST --data-binary @"$SCRIPT_PATH"/data/metadatablocks/journals.tsv -H "Content-type: text/tab-separated-values" diff --git a/scripts/api/setup-identity-providers.sh b/scripts/api/setup-identity-providers.sh index 89ac59de32f..e877f71c6b0 100755 --- a/scripts/api/setup-identity-providers.sh +++ b/scripts/api/setup-identity-providers.sh @@ -1,8 +1,11 @@ -SERVER=http://localhost:8080/api +#!/bin/bash + +DATAVERSE_URL=${DATAVERSE_URL:-"http://localhost:8080"} +SCRIPT_PATH="$(dirname "$0")" # Setup the authentication providers echo "Setting up internal user provider" -curl -H "Content-type:application/json" -d @data/authentication-providers/builtin.json http://localhost:8080/api/admin/authenticationProviders/ +curl -H "Content-type:application/json" -d @"$SCRIPT_PATH"/data/authentication-providers/builtin.json "${DATAVERSE_URL}/api/admin/authenticationProviders/" #echo "Setting up Echo providers" #curl -H "Content-type:application/json" -d @data/authentication-providers/echo.json http://localhost:8080/api/admin/authenticationProviders/ diff --git a/scripts/dev/docker-final-setup.sh b/scripts/dev/docker-final-setup.sh new file mode 100755 index 00000000000..d2453619ec2 --- /dev/null +++ b/scripts/dev/docker-final-setup.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +set -euo pipefail + +echo "Running setup-all.sh (INSECURE MODE)..." +cd scripts/api || exit +./setup-all.sh --insecure -p=admin1 | tee /tmp/setup-all.sh.out +cd ../.. + +echo "Setting system mail address..." +curl -X PUT -d "dataverse@localhost" "http://localhost:8080/api/admin/settings/:SystemEmail" + +echo "Setting DOI provider to \"FAKE\"..." +curl "http://localhost:8080/api/admin/settings/:DoiProvider" -X PUT -d FAKE + +API_TOKEN=$(grep apiToken "/tmp/setup-all.sh.out" | jq ".data.apiToken" | tr -d \") +export API_TOKEN + +echo "Publishing root dataverse..." +curl -H "X-Dataverse-key:$API_TOKEN" -X POST "http://localhost:8080/api/dataverses/:root/actions/:publish" + +echo "Allowing users to create dataverses and datasets in root..." +curl -H "X-Dataverse-key:$API_TOKEN" -X POST -H "Content-type:application/json" -d "{\"assignee\": \":authenticated-users\",\"role\": \"fullContributor\"}" "http://localhost:8080/api/dataverses/:root/assignments" + +echo "Checking Dataverse version..." +curl "http://localhost:8080/api/info/version" \ No newline at end of file diff --git a/scripts/installer/as-setup.sh b/scripts/installer/as-setup.sh index 853db77f471..49ebce059d2 100755 --- a/scripts/installer/as-setup.sh +++ b/scripts/installer/as-setup.sh @@ -106,13 +106,13 @@ function preliminary_setup() # (we can no longer offer EZID with their shared test account) # jvm-options use colons as separators, escape as literal DOI_BASEURL_ESC=`echo $DOI_BASEURL | sed -e 's/:/\\\:/'` - ./asadmin $ASADMIN_OPTS create-jvm-options "\-Ddoi.username=${DOI_USERNAME}" - ./asadmin $ASADMIN_OPTS create-jvm-options '\-Ddoi.password=${ALIAS=doi_password_alias}' - ./asadmin $ASADMIN_OPTS create-jvm-options "\-Ddoi.baseurlstring=$DOI_BASEURL_ESC" + ./asadmin $ASADMIN_OPTS create-jvm-options "\-Ddataverse.pid.datacite.username=${DOI_USERNAME}" + ./asadmin $ASADMIN_OPTS create-jvm-options '\-Ddataverse.pid.datacite.password=${ALIAS=doi_password_alias}' + ./asadmin $ASADMIN_OPTS create-jvm-options "\-Ddataverse.pid.datacite.mds-api-url=$DOI_BASEURL_ESC" # jvm-options use colons as separators, escape as literal DOI_DATACITERESTAPIURL_ESC=`echo $DOI_DATACITERESTAPIURL | sed -e 's/:/\\\:/'` - ./asadmin $ASADMIN_OPTS create-jvm-options "\-Ddoi.dataciterestapiurlstring=$DOI_DATACITERESTAPIURL_ESC" + ./asadmin $ASADMIN_OPTS create-jvm-options "\-Ddataverse.pid.datacite.rest-api-url=$DOI_DATACITERESTAPIURL_ESC" ./asadmin $ASADMIN_OPTS create-jvm-options "-Ddataverse.timerServer=true" diff --git a/scripts/installer/install.py b/scripts/installer/install.py index ea1a69db6a7..5acb4d760a4 100644 --- a/scripts/installer/install.py +++ b/scripts/installer/install.py @@ -578,8 +578,8 @@ print("However, you have to contact DataCite (support\@datacite.org) and request a test account, before you ") print("can publish datasets. Once you receive the account name and password, add them to your domain.xml,") print("as the following two JVM options:") -print("\t-Ddoi.username=...") -print("\t-Ddoi.password=...") +print("\t-Ddataverse.pid.datacite.username=...") +print("\t-Ddataverse.pid.datacite.password=...") print("and restart payara") print("If this is a production Dataverse and you are planning to register datasets as ") print("\"real\", non-test DOIs or Handles, consult the \"Persistent Identifiers and Publishing Datasets\"") diff --git a/scripts/search/tests/data/dataset-finch1-nolicense.json b/scripts/search/tests/data/dataset-finch1-nolicense.json new file mode 100644 index 00000000000..ec0856a2aa3 --- /dev/null +++ b/scripts/search/tests/data/dataset-finch1-nolicense.json @@ -0,0 +1,77 @@ +{ + "datasetVersion": { + "metadataBlocks": { + "citation": { + "fields": [ + { + "value": "Darwin's Finches", + "typeClass": "primitive", + "multiple": false, + "typeName": "title" + }, + { + "value": [ + { + "authorName": { + "value": "Finch, Fiona", + "typeClass": "primitive", + "multiple": false, + "typeName": "authorName" + }, + "authorAffiliation": { + "value": "Birds Inc.", + "typeClass": "primitive", + "multiple": false, + "typeName": "authorAffiliation" + } + } + ], + "typeClass": "compound", + "multiple": true, + "typeName": "author" + }, + { + "value": [ + { "datasetContactEmail" : { + "typeClass": "primitive", + "multiple": false, + "typeName": "datasetContactEmail", + "value" : "finch@mailinator.com" + }, + "datasetContactName" : { + "typeClass": "primitive", + "multiple": false, + "typeName": "datasetContactName", + "value": "Finch, Fiona" + } + }], + "typeClass": "compound", + "multiple": true, + "typeName": "datasetContact" + }, + { + "value": [ { + "dsDescriptionValue":{ + "value": "Darwin's finches (also known as the Galápagos finches) are a group of about fifteen species of passerine birds.", + "multiple":false, + "typeClass": "primitive", + "typeName": "dsDescriptionValue" + }}], + "typeClass": "compound", + "multiple": true, + "typeName": "dsDescription" + }, + { + "value": [ + "Medicine, Health and Life Sciences" + ], + "typeClass": "controlledVocabulary", + "multiple": true, + "typeName": "subject" + } + ], + "displayName": "Citation Metadata" + } + } + } +} diff --git a/scripts/search/tests/data/dataset-finch1.json b/scripts/search/tests/data/dataset-finch1.json index ec0856a2aa3..433ea758711 100644 --- a/scripts/search/tests/data/dataset-finch1.json +++ b/scripts/search/tests/data/dataset-finch1.json @@ -1,5 +1,9 @@ { "datasetVersion": { + "license": { + "name": "CC0 1.0", + "uri": "http://creativecommons.org/publicdomain/zero/1.0" + }, "metadataBlocks": { "citation": { "fields": [ diff --git a/scripts/search/tests/data/dataset-finch2.json b/scripts/search/tests/data/dataset-finch2.json index d20f835b629..446df54676a 100644 --- a/scripts/search/tests/data/dataset-finch2.json +++ b/scripts/search/tests/data/dataset-finch2.json @@ -1,5 +1,9 @@ { "datasetVersion": { + "license": { + "name": "CC0 1.0", + "uri": "http://creativecommons.org/publicdomain/zero/1.0" + }, "metadataBlocks": { "citation": { "fields": [ diff --git a/src/main/docker/Dockerfile b/src/main/docker/Dockerfile new file mode 100644 index 00000000000..88020a118b5 --- /dev/null +++ b/src/main/docker/Dockerfile @@ -0,0 +1,54 @@ +# Copyright 2023 Forschungszentrum Jülich GmbH +# Licensed 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 +# +################################################################################################################ +# +# THIS FILE IS TO BE USED WITH MAVEN DOCKER BUILD: +# mvn -Pct clean package +# +################################################################################################################ +# +# Some commands used are inspired by https://github.com/payara/Payara/tree/master/appserver/extras/docker-images. +# Most parts origin from older versions of https://github.com/gdcc/dataverse-kubernetes. +# +# We are not using upstream Payara images because: +# - Their image is less optimised for production usage and Dataverse by design choices +# - We provide multi-arch images +# - We provide some tweaks for development and monitoring +# + +# Make the Java base image and version configurable (useful for trying newer Java versions and flavors) +ARG BASE_IMAGE="gdcc/base:unstable" +FROM $BASE_IMAGE + +# Make Payara use the "ct" profile for MicroProfile Config. Will switch various defaults for the application +# setup in META-INF/microprofile-config.properties. +# See also https://download.eclipse.org/microprofile/microprofile-config-3.0/microprofile-config-spec-3.0.html#configprofile +ENV MP_CONFIG_PROFILE=ct + +# Copy app and deps from assembly in proper layers +COPY --chown=payara:payara maven/deps ${DEPLOY_DIR}/dataverse/WEB-INF/lib/ +COPY --chown=payara:payara maven/app ${DEPLOY_DIR}/dataverse/ +COPY --chown=payara:payara maven/supplements ${DEPLOY_DIR}/dataverse/supplements/ +COPY --chown=payara:payara maven/scripts ${SCRIPT_DIR}/ +RUN chmod +x "${SCRIPT_DIR}"/* + +# Create symlinks for jHove +RUN ln -s "${DEPLOY_DIR}/dataverse/supplements/jhove.conf" "${PAYARA_DIR}/glassfish/domains/${DOMAIN_NAME}/config/jhove.conf" && \ + ln -s "${DEPLOY_DIR}/dataverse/supplements/jhoveConfig.xsd" "${PAYARA_DIR}/glassfish/domains/${DOMAIN_NAME}/config/jhoveConfig.xsd" && \ + sed -i "${PAYARA_DIR}/glassfish/domains/${DOMAIN_NAME}/config/jhove.conf" -e "s:/usr/local/payara./glassfish/domains/domain1:${PAYARA_DIR}/glassfish/domains/${DOMAIN_NAME}:g" + +LABEL org.opencontainers.image.created="@git.build.time@" \ + org.opencontainers.image.authors="Research Data Management at FZJ " \ + org.opencontainers.image.url="https://guides.dataverse.org/en/latest/container/" \ + org.opencontainers.image.documentation="https://guides.dataverse.org/en/latest/container/" \ + org.opencontainers.image.source="https://github.com/IQSS/dataverse" \ + org.opencontainers.image.version="@project.version@" \ + org.opencontainers.image.revision="@git.commit.id.abbrev@" \ + org.opencontainers.image.vendor="Global Dataverse Community Consortium" \ + org.opencontainers.image.licenses="Apache-2.0" \ + org.opencontainers.image.title="Dataverse Application Image" \ + org.opencontainers.image.description="This container image provides the research data repository software Dataverse in a box." \ No newline at end of file diff --git a/src/main/docker/README.md b/src/main/docker/README.md new file mode 100644 index 00000000000..06e2769ed6e --- /dev/null +++ b/src/main/docker/README.md @@ -0,0 +1,62 @@ +# Dataverse Application Container Image + +The "application image" offers you a deployment-ready Dataverse application running on the underlying +application server, which is provided by the [base image](https://hub.docker.com/r/gdcc/base). +Its sole purpose is to bundle the application and any additional material necessary to successfully jumpstart +the application. + +Note: Until all :ref:`jvm-options` are *MicroProfile Config* enabled, it also adds the necessary scripting glue to +configure the applications domain during booting the application server. See :ref:`app-tunables`. + +## Quick Reference + +**Maintained by:** + +This image is created, maintained and supported by the Dataverse community on a best-effort basis. + +**Where to find documentation:** + +The [Dataverse Container Guide - Application Image](https://guides.dataverse.org/en/latest/container/app-image.html) +provides in-depth information about content, building, tuning and so on for this image. You should also consult +the [Dataverse Container Guide - Base Image](https://guides.dataverse.org/en/latest/container/base-image.html) page +for more details on tunable settings, locations, etc. + +**Where to get help and ask questions:** + +IQSS will not offer support on how to deploy or run it. Please reach out to the community for help on using it. +You can join the Community Chat on Matrix at https://chat.dataverse.org and https://groups.google.com/g/dataverse-community +to ask for help and guidance. + +## Supported Image Tags + +This image is sourced within the main upstream code [repository of the Dataverse software](https://github.com/IQSS/dataverse). +Development and maintenance of the [image's code](https://github.com/IQSS/dataverse/tree/develop/src/main/docker) +happens there (again, by the community). Community-supported image tags are based on the two most important branches: + +- The `unstable` tag corresponds to the `develop` branch, where pull requests are merged. + ([`Dockerfile`](https://github.com/IQSS/dataverse/tree/develop/src/main/docker/Dockerfile)) +- The `alpha` tag corresponds to the `master` branch, where releases are cut from. + ([`Dockerfile`](https://github.com/IQSS/dataverse/tree/master/src/main/docker/Dockerfile)) + +Within the main repository, you may find the application image files at `/src/main/docker`. +This Maven module uses the [Maven Docker Plugin](https://dmp.fabric8.io) to build and ship the image. +You may use, extend, or alter this image to your liking and/or host in some different registry if you want to. + +**Supported architectures:** This image is created as a "multi-arch image", supporting the most common architectures +Dataverse usually runs on: AMD64 (Windows/Linux/...) and ARM64 (Apple M1/M2). + +## License + +Image content created by the community is licensed under [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0), +like the [main Dataverse project](https://github.com/IQSS/dataverse/blob/develop/LICENSE.md). + +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. + +As with all Docker images, all images likely also contain other software which may be under other licenses (such as +[Payara Server](https://github.com/payara/Payara/blob/master/LICENSE.txt), Bash, etc., from the base +distribution, along with any direct or indirect (Java) dependencies contained). + +As for any pre-built image usage, it is the image user's responsibility to ensure that any use of this image complies +with any relevant licenses for all software contained within. diff --git a/src/main/docker/assembly.xml b/src/main/docker/assembly.xml new file mode 100644 index 00000000000..9f9b39617a3 --- /dev/null +++ b/src/main/docker/assembly.xml @@ -0,0 +1,28 @@ + + + + + target/${project.artifactId}-${project.version} + app + + WEB-INF/lib/**/* + + + + + target/${project.artifactId}-${project.version}/WEB-INF/lib + deps + + + + conf/jhove + supplements + + + + src/main/docker/scripts + scripts + + + \ No newline at end of file diff --git a/src/main/docker/scripts/init_2_configure.sh b/src/main/docker/scripts/init_2_configure.sh new file mode 100755 index 00000000000..a98f08088c1 --- /dev/null +++ b/src/main/docker/scripts/init_2_configure.sh @@ -0,0 +1,64 @@ +#!/bin/bash +################################################################################ +# Configure Payara +# +# BEWARE: As this is done for Kubernetes, we will ALWAYS start with a fresh container! +# When moving to Payara 5+ the option commands are idempotent. +# The resources are to be created by the application on deployment, +# once Dataverse has proper refactoring, etc. +################################################################################ + +# Fail on any error +set -euo pipefail + +# Include some sane defaults (which are currently not settable via MicroProfile Config). +# This is an ugly hack and shall be removed once #7000 is resolved. +export dataverse_auth_password__reset__timeout__in__minutes="${dataverse_auth_password__reset__timeout__in__minutes:-60}" +export dataverse_timerServer="${dataverse_timerServer:-true}" +export dataverse_files_storage__driver__id="${dataverse_files_storage__driver__id:-local}" +if [ "${dataverse_files_storage__driver__id}" = "local" ]; then + export dataverse_files_local_type="${dataverse_files_local_type:-file}" + export dataverse_files_local_label="${dataverse_files_local_label:-Local}" + export dataverse_files_local_directory="${dataverse_files_local_directory:-${STORAGE_DIR}/store}" +fi + +# 0. Define postboot commands file to be read by Payara and clear it +DV_POSTBOOT=${PAYARA_DIR}/dataverse_postboot +echo "# Dataverse postboot configuration for Payara" > "${DV_POSTBOOT}" + +# 2. Domain-spaced resources (JDBC, JMS, ...) +# TODO: This is ugly and dirty. It should be replaced with resources from +# EE 8 code annotations or at least glassfish-resources.xml +# NOTE: postboot commands is not multi-line capable, thus spaghetti needed. + +# JavaMail +echo "INFO: Defining JavaMail." +echo "create-javamail-resource --mailhost=${DATAVERSE_MAIL_HOST:-smtp} --mailuser=${DATAVERSE_MAIL_USER:-dataversenotify} --fromaddress=${DATAVERSE_MAIL_FROM:-dataverse@localhost} mail/notifyMailSession" >> "${DV_POSTBOOT}" + +# 3. Domain based configuration options +# Set Dataverse environment variables +echo "INFO: Defining system properties for Dataverse configuration options." +#env | grep -Ee "^(dataverse|doi)_" | sort -fd +env -0 | grep -z -Ee "^(dataverse|doi)_" | while IFS='=' read -r -d '' k v; do + # transform __ to - + # shellcheck disable=SC2001 + KEY=$(echo "${k}" | sed -e "s#__#-#g") + # transform remaining single _ to . + KEY=$(echo "${KEY}" | tr '_' '.') + + # escape colons in values + # shellcheck disable=SC2001 + v=$(echo "${v}" | sed -e 's/:/\\\:/g') + + echo "DEBUG: Handling ${KEY}=${v}." + echo "create-system-properties ${KEY}=${v}" >> "${DV_POSTBOOT}" +done + +# 4. Add the commands to the existing postboot file, but insert BEFORE deployment +TMPFILE=$(mktemp) +cat "${DV_POSTBOOT}" "${POSTBOOT_COMMANDS}" > "${TMPFILE}" && mv "${TMPFILE}" "${POSTBOOT_COMMANDS}" +echo "DEBUG: postboot contains the following commands:" +echo "--------------------------------------------------" +cat "${POSTBOOT_COMMANDS}" +echo "--------------------------------------------------" + diff --git a/src/main/java/edu/harvard/iq/dataverse/AbstractGlobalIdServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/AbstractGlobalIdServiceBean.java index f6cbd01ece0..2a3f2d50364 100644 --- a/src/main/java/edu/harvard/iq/dataverse/AbstractGlobalIdServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/AbstractGlobalIdServiceBean.java @@ -3,11 +3,13 @@ import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.SystemConfig; import java.io.InputStream; - import javax.ejb.EJB; +import javax.inject.Inject; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; + +import org.apache.commons.lang3.RandomStringUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; @@ -17,27 +19,21 @@ public abstract class AbstractGlobalIdServiceBean implements GlobalIdServiceBean private static final Logger logger = Logger.getLogger(AbstractGlobalIdServiceBean.class.getCanonicalName()); - @EJB + @Inject DataverseServiceBean dataverseService; @EJB + protected SettingsServiceBean settingsService; - @EJB - EjbDataverseEngine commandEngine; - @EJB - DatasetServiceBean datasetService; - @EJB - DataFileServiceBean datafileService; - @EJB + @Inject + protected + DvObjectServiceBean dvObjectService; + @Inject SystemConfig systemConfig; + + protected Boolean configured = null; public static String UNAVAILABLE = ":unav"; - @Override - public String getIdentifierForLookup(String protocol, String authority, String identifier) { - logger.log(Level.FINE,"getIdentifierForLookup"); - return protocol + ":" + authority + "/" + identifier; - } - @Override public Map getMetadataForCreateIndicator(DvObject dvObjectIn) { logger.log(Level.FINE,"getMetadataForCreateIndicator(DvObject)"); @@ -101,14 +97,10 @@ protected String getTargetUrl(DvObject dvObjectIn) { @Override public String getIdentifier(DvObject dvObject) { - return dvObject.getGlobalId().asString(); + GlobalId gid = dvObject.getGlobalId(); + return gid != null ? gid.asString() : null; } - protected String getTargetUrl(Dataset datasetIn) { - logger.log(Level.FINE,"getTargetUrl"); - return systemConfig.getDataverseSiteUrl() + Dataset.TARGET_URL + datasetIn.getGlobalIdString(); - } - protected String generateYear (DvObject dvObjectIn){ return dvObjectIn.getYearPublishedCreated(); } @@ -120,16 +112,41 @@ public Map getMetadataForTargetURL(DvObject dvObject) { return metadata; } + @Override + public boolean alreadyRegistered(DvObject dvo) throws Exception { + if(dvo==null) { + logger.severe("Null DvObject sent to alreadyRegistered()."); + return false; + } + GlobalId globalId = dvo.getGlobalId(); + if(globalId == null) { + return false; + } + return alreadyRegistered(globalId, false); + } + + public abstract boolean alreadyRegistered(GlobalId globalId, boolean noProviderDefault) throws Exception; + + /* + * ToDo: the DvObject being sent in provides partial support for the case where + * it has a different authority/protocol than what is configured (i.e. a legacy + * Pid that can actually be updated by the Pid account being used.) Removing + * this now would potentially break/make it harder to handle that case prior to + * support for configuring multiple Pid providers. Once that exists, it would be + * cleaner to always find the PidProvider associated with the + * protocol/authority/shoulder of the current dataset and then not pass the + * DvObject as a param. (This would also remove calls to get the settings since + * that would be done at construction.) + */ @Override public DvObject generateIdentifier(DvObject dvObject) { String protocol = dvObject.getProtocol() == null ? settingsService.getValueForKey(SettingsServiceBean.Key.Protocol) : dvObject.getProtocol(); String authority = dvObject.getAuthority() == null ? settingsService.getValueForKey(SettingsServiceBean.Key.Authority) : dvObject.getAuthority(); - GlobalIdServiceBean idServiceBean = GlobalIdServiceBean.getBean(protocol, commandEngine.getContext()); if (dvObject.isInstanceofDataset()) { - dvObject.setIdentifier(datasetService.generateDatasetIdentifier((Dataset) dvObject, idServiceBean)); + dvObject.setIdentifier(generateDatasetIdentifier((Dataset) dvObject)); } else { - dvObject.setIdentifier(datafileService.generateDataFileIdentifier((DataFile) dvObject, idServiceBean)); + dvObject.setIdentifier(generateDataFileIdentifier((DataFile) dvObject)); } if (dvObject.getProtocol() == null) { dvObject.setProtocol(protocol); @@ -140,6 +157,227 @@ public DvObject generateIdentifier(DvObject dvObject) { return dvObject; } + //ToDo just send the DvObject.DType + public String generateDatasetIdentifier(Dataset dataset) { + //ToDo - track these in the bean + String identifierType = settingsService.getValueForKey(SettingsServiceBean.Key.IdentifierGenerationStyle, "randomString"); + String shoulder = settingsService.getValueForKey(SettingsServiceBean.Key.Shoulder, ""); + + switch (identifierType) { + case "randomString": + return generateIdentifierAsRandomString(dataset, shoulder); + case "storedProcGenerated": + return generateIdentifierFromStoredProcedureIndependent(dataset, shoulder); + default: + /* Should we throw an exception instead?? -- L.A. 4.6.2 */ + return generateIdentifierAsRandomString(dataset, shoulder); + } + } + + + /** + * Check that a identifier entered by the user is unique (not currently used + * for any other study in this Dataverse Network) also check for duplicate + * in EZID if needed + * @param userIdentifier + * @param dataset + * @return {@code true} if the identifier is unique, {@code false} otherwise. + */ + public boolean isGlobalIdUnique(GlobalId globalId) { + if ( ! dvObjectService.isGlobalIdLocallyUnique(globalId) ) { + return false; // duplication found in local database + } + + // not in local DB, look in the persistent identifier service + try { + return ! alreadyRegistered(globalId, false); + } catch (Exception e){ + //we can live with failure - means identifier not found remotely + } + + return true; + } + + /** + * Parse a Persistent Id and set the protocol, authority, and identifier + * + * Example 1: doi:10.5072/FK2/BYM3IW + * protocol: doi + * authority: 10.5072 + * identifier: FK2/BYM3IW + * + * Example 2: hdl:1902.1/111012 + * protocol: hdl + * authority: 1902.1 + * identifier: 111012 + * + * @param identifierString + * @param separator the string that separates the authority from the identifier. + * @param destination the global id that will contain the parsed data. + * @return {@code destination}, after its fields have been updated, or + * {@code null} if parsing failed. + */ + @Override + public GlobalId parsePersistentId(String fullIdentifierString) { + if(!isConfigured()) { + return null; + } + int index1 = fullIdentifierString.indexOf(':'); + if (index1 > 0) { // ':' found with one or more characters before it + String protocol = fullIdentifierString.substring(0, index1); + GlobalId globalId = parsePersistentId(protocol, fullIdentifierString.substring(index1+1)); + return globalId; + } + logger.log(Level.INFO, "Error parsing identifier: {0}: '':'' not found in string", fullIdentifierString); + return null; + } + + protected GlobalId parsePersistentId(String protocol, String identifierString) { + if(!isConfigured()) { + return null; + } + String authority; + String identifier; + if (identifierString == null) { + return null; + } + int index = identifierString.indexOf('/'); + if (index > 0 && (index + 1) < identifierString.length()) { + // '/' found with one or more characters + // before and after it + // Strip any whitespace, ; and ' from authority (should finding them cause a + // failure instead?) + authority = GlobalIdServiceBean.formatIdentifierString(identifierString.substring(0, index)); + if (GlobalIdServiceBean.testforNullTerminator(authority)) { + return null; + } + identifier = GlobalIdServiceBean.formatIdentifierString(identifierString.substring(index + 1)); + if (GlobalIdServiceBean.testforNullTerminator(identifier)) { + return null; + } + } else { + logger.log(Level.INFO, "Error parsing identifier: {0}: '':/'' not found in string", + identifierString); + return null; + } + return parsePersistentId(protocol, authority, identifier); + } + + public GlobalId parsePersistentId(String protocol, String authority, String identifier) { + if(!isConfigured()) { + return null; + } + logger.fine("Parsing: " + protocol + ":" + authority + getSeparator() + identifier + " in " + getProviderInformation().get(0)); + if(!GlobalIdServiceBean.isValidGlobalId(protocol, authority, identifier)) { + return null; + } + return new GlobalId(protocol, authority, identifier, getSeparator(), getUrlPrefix(), + getProviderInformation().get(0)); + } + + + public String getSeparator() { + //The standard default + return "/"; + } + + @Override + public String generateDataFileIdentifier(DataFile datafile) { + String doiIdentifierType = settingsService.getValueForKey(SettingsServiceBean.Key.IdentifierGenerationStyle, "randomString"); + String doiDataFileFormat = settingsService.getValueForKey(SettingsServiceBean.Key.DataFilePIDFormat, SystemConfig.DataFilePIDFormat.DEPENDENT.toString()); + + String prepend = ""; + if (doiDataFileFormat.equals(SystemConfig.DataFilePIDFormat.DEPENDENT.toString())){ + //If format is dependent then pre-pend the dataset identifier + prepend = datafile.getOwner().getIdentifier() + "/"; + datafile.setProtocol(datafile.getOwner().getProtocol()); + datafile.setAuthority(datafile.getOwner().getAuthority()); + } else { + //If there's a shoulder prepend independent identifiers with it + prepend = settingsService.getValueForKey(SettingsServiceBean.Key.Shoulder, ""); + datafile.setProtocol(settingsService.getValueForKey(SettingsServiceBean.Key.Protocol)); + datafile.setAuthority(settingsService.getValueForKey(SettingsServiceBean.Key.Authority)); + } + + switch (doiIdentifierType) { + case "randomString": + return generateIdentifierAsRandomString(datafile, prepend); + case "storedProcGenerated": + if (doiDataFileFormat.equals(SystemConfig.DataFilePIDFormat.INDEPENDENT.toString())){ + return generateIdentifierFromStoredProcedureIndependent(datafile, prepend); + } else { + return generateIdentifierFromStoredProcedureDependent(datafile, prepend); + } + default: + /* Should we throw an exception instead?? -- L.A. 4.6.2 */ + return generateIdentifierAsRandomString(datafile, prepend); + } + } + + + /* + * This method checks locally for a DvObject with the same PID and if that is OK, checks with the PID service. + * @param dvo - the object to check (ToDo - get protocol/authority from this PidProvider object) + * @param prepend - for Datasets, this is always the shoulder, for DataFiles, it could be the shoulder or the parent Dataset identifier + */ + private String generateIdentifierAsRandomString(DvObject dvo, String prepend) { + String identifier = null; + do { + identifier = prepend + RandomStringUtils.randomAlphanumeric(6).toUpperCase(); + } while (!isGlobalIdUnique(new GlobalId(dvo.getProtocol(), dvo.getAuthority(), identifier, this.getSeparator(), this.getUrlPrefix(), this.getProviderInformation().get(0)))); + + return identifier; + } + + /* + * This method checks locally for a DvObject with the same PID and if that is OK, checks with the PID service. + * @param dvo - the object to check (ToDo - get protocol/authority from this PidProvider object) + * @param prepend - for Datasets, this is always the shoulder, for DataFiles, it could be the shoulder or the parent Dataset identifier + */ + + private String generateIdentifierFromStoredProcedureIndependent(DvObject dvo, String prepend) { + String identifier; + do { + String identifierFromStoredProcedure = dvObjectService.generateNewIdentifierByStoredProcedure(); + // some diagnostics here maybe - is it possible to determine that it's failing + // because the stored procedure hasn't been created in the database? + if (identifierFromStoredProcedure == null) { + return null; + } + identifier = prepend + identifierFromStoredProcedure; + } while (!isGlobalIdUnique(new GlobalId(dvo.getProtocol(), dvo.getAuthority(), identifier, this.getSeparator(), this.getUrlPrefix(), this.getProviderInformation().get(0)))); + + return identifier; + } + + /*This method is only used for DataFiles with DEPENDENT Pids. It is not for Datasets + * + */ + private String generateIdentifierFromStoredProcedureDependent(DataFile datafile, String prepend) { + String identifier; + Long retVal; + retVal = Long.valueOf(0L); + //ToDo - replace loops with one lookup for largest entry? (the do loop runs ~n**2/2 calls). The check for existingIdentifiers means this is mostly a local loop now, versus involving db or PidProvider calls, but still...) + + // This will catch identifiers already assigned in the current transaction (e.g. + // in FinalizeDatasetPublicationCommand) that haven't been committed to the db + // without having to make a call to the PIDProvider + Set existingIdentifiers = new HashSet(); + List files = datafile.getOwner().getFiles(); + for(DataFile f:files) { + existingIdentifiers.add(f.getIdentifier()); + } + + do { + retVal++; + identifier = prepend + retVal.toString(); + + } while (existingIdentifiers.contains(identifier) || !isGlobalIdUnique(new GlobalId(datafile.getProtocol(), datafile.getAuthority(), identifier, this.getSeparator(), this.getUrlPrefix(), this.getProviderInformation().get(0)))); + + return identifier; + } + + class GlobalIdMetadataTemplate { @@ -159,7 +397,6 @@ public GlobalIdMetadataTemplate(){ private String xmlMetadata; private String identifier; - private String datasetIdentifier; private List datafileIdentifiers; private List creators; private String title; @@ -245,7 +482,7 @@ public String generateXML(DvObject dvObject) { // Added to prevent a NullPointerException when trying to destroy datasets when using DataCite rather than EZID. publisherYearFinal = this.publisherYear; } - xmlMetadata = template.replace("${identifier}", this.identifier.trim()) + xmlMetadata = template.replace("${identifier}", getIdentifier().trim()) .replace("${title}", this.title) .replace("${publisher}", this.publisher) .replace("${publisherYear}", publisherYearFinal) @@ -371,10 +608,6 @@ public void setIdentifier(String identifier) { this.identifier = identifier; } - public void setDatasetIdentifier(String datasetIdentifier) { - this.datasetIdentifier = datasetIdentifier; - } - public List getCreators() { return creators; } @@ -428,10 +661,6 @@ public String getMetadataFromDvObject(String identifier, Map met DataFile df = (DataFile) dvObject; String fileDescription = df.getDescription(); metadataTemplate.setDescription(fileDescription == null ? "" : fileDescription); - String datasetPid = df.getOwner().getGlobalId().asString(); - metadataTemplate.setDatasetIdentifier(datasetPid); - } else { - metadataTemplate.setDatasetIdentifier(""); } metadataTemplate.setContacts(dataset.getLatestVersion().getDatasetContacts()); @@ -448,5 +677,19 @@ public String getMetadataFromDvObject(String identifier, Map met logger.log(Level.FINE, "XML to send to DataCite: {0}", xmlMetadata); return xmlMetadata; } + + @Override + public boolean canManagePID() { + //The default expectation is that PID providers are configured to manage some set (i.e. based on protocol/authority/shoulder) of PIDs + return true; + } + @Override + public boolean isConfigured() { + if(configured==null) { + return false; + } else { + return configured.booleanValue(); + } + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/CitationServlet.java b/src/main/java/edu/harvard/iq/dataverse/CitationServlet.java index 2b342b09610..f6b4e3dc99a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/CitationServlet.java +++ b/src/main/java/edu/harvard/iq/dataverse/CitationServlet.java @@ -5,6 +5,7 @@ */ package edu.harvard.iq.dataverse; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; import edu.harvard.iq.dataverse.util.StringUtil; import java.io.IOException; import java.io.PrintWriter; @@ -21,7 +22,7 @@ public class CitationServlet extends HttpServlet { @EJB - DatasetServiceBean datasetService; + DvObjectServiceBean dvObjectService; /** * Processes requests for both HTTP GET and POST @@ -37,10 +38,14 @@ protected void processRequest(HttpServletRequest request, HttpServletResponse re String persistentId = request.getParameter("persistentId"); if (persistentId != null) { - Dataset ds = datasetService.findByGlobalId(persistentId); - if (ds != null) { - response.sendRedirect("dataset.xhtml?persistentId=" + persistentId); - return; + DvObject dob = dvObjectService.findByGlobalId(PidUtil.parseAsGlobalID(persistentId)); + if (dob != null) { + if (dob instanceof Dataset) { + response.sendRedirect("dataset.xhtml?persistentId=" + persistentId); + } else if (dob instanceof DataFile) { + response.sendRedirect("file.xhtml?persistentId=" + persistentId); + } + return; } } response.sendError(HttpServletResponse.SC_NOT_FOUND); diff --git a/src/main/java/edu/harvard/iq/dataverse/DOIDataCiteRegisterService.java b/src/main/java/edu/harvard/iq/dataverse/DOIDataCiteRegisterService.java index 218e4c85474..b748897dafe 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DOIDataCiteRegisterService.java +++ b/src/main/java/edu/harvard/iq/dataverse/DOIDataCiteRegisterService.java @@ -23,6 +23,8 @@ import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.TypedQuery; + +import edu.harvard.iq.dataverse.settings.JvmSettings; import org.apache.commons.text.StringEscapeUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; @@ -53,7 +55,11 @@ public class DOIDataCiteRegisterService { private DataCiteRESTfullClient getClient() throws IOException { if (client == null) { - client = new DataCiteRESTfullClient(System.getProperty("doi.baseurlstring"), System.getProperty("doi.username"), System.getProperty("doi.password")); + client = new DataCiteRESTfullClient( + JvmSettings.DATACITE_MDS_API_URL.lookup(), + JvmSettings.DATACITE_USERNAME.lookup(), + JvmSettings.DATACITE_PASSWORD.lookup() + ); } return client; } @@ -546,7 +552,7 @@ private String generateRelatedIdentifiers(DvObject dvObject) { datafileIdentifiers = new ArrayList<>(); for (DataFile dataFile : dataset.getFiles()) { - if (!dataFile.getGlobalId().asString().isEmpty()) { + if (dataFile.getGlobalId() != null) { if (sb.toString().isEmpty()) { sb.append(""); } diff --git a/src/main/java/edu/harvard/iq/dataverse/DOIDataCiteServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DOIDataCiteServiceBean.java index e7dd49a6926..fa0a745d80f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DOIDataCiteServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DOIDataCiteServiceBean.java @@ -10,9 +10,11 @@ import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; + import javax.ejb.EJB; import javax.ejb.Stateless; +import edu.harvard.iq.dataverse.settings.JvmSettings; import org.apache.commons.httpclient.HttpException; import org.apache.commons.httpclient.HttpStatus; @@ -22,7 +24,7 @@ * @author luopc */ @Stateless -public class DOIDataCiteServiceBean extends AbstractGlobalIdServiceBean { +public class DOIDataCiteServiceBean extends DOIServiceBean { private static final Logger logger = Logger.getLogger(DOIDataCiteServiceBean.class.getCanonicalName()); @@ -34,41 +36,30 @@ public class DOIDataCiteServiceBean extends AbstractGlobalIdServiceBean { @EJB DOIDataCiteRegisterService doiDataCiteRegisterService; - public DOIDataCiteServiceBean() { - } - @Override public boolean registerWhenPublished() { return false; } - @Override - public boolean alreadyExists(DvObject dvObject) { - if(dvObject==null) { - logger.severe("Null DvObject sent to alreadyExists()."); - return false; - } - return alreadyExists(dvObject.getGlobalId()); - } + @Override - public boolean alreadyExists(GlobalId pid) { - logger.log(Level.FINE,"alreadyExists"); + public boolean alreadyRegistered(GlobalId pid, boolean noProviderDefault) { + logger.log(Level.FINE,"alreadyRegistered"); if(pid==null || pid.asString().isEmpty()) { logger.fine("No identifier sent."); return false; } - boolean alreadyExists; + boolean alreadyRegistered; String identifier = pid.asString(); try{ - alreadyExists = doiDataCiteRegisterService.testDOIExists(identifier); + alreadyRegistered = doiDataCiteRegisterService.testDOIExists(identifier); } catch (Exception e){ - logger.log(Level.WARNING, "alreadyExists failed"); + logger.log(Level.WARNING, "alreadyRegistered failed"); return false; } - return alreadyExists; + return alreadyRegistered; } - @Override public String createIdentifier(DvObject dvObject) throws Exception { @@ -90,10 +81,10 @@ public String createIdentifier(DvObject dvObject) throws Exception { } @Override - public HashMap getIdentifierMetadata(DvObject dvObject) { + public Map getIdentifierMetadata(DvObject dvObject) { logger.log(Level.FINE,"getIdentifierMetadata"); String identifier = getIdentifier(dvObject); - HashMap metadata = new HashMap<>(); + Map metadata = new HashMap<>(); try { metadata = doiDataCiteRegisterService.getMetadata(identifier); } catch (Exception e) { @@ -103,29 +94,6 @@ public HashMap getIdentifierMetadata(DvObject dvObject) { } - /** - * Looks up the metadata for a Global Identifier - * @param protocol the identifier system, e.g. "doi" - * @param authority the namespace that the authority manages in the identifier system - * @param identifier the local identifier part - * @return a Map of metadata. It is empty when the lookup failed, e.g. when - * the identifier does not exist. - */ - @Override - public HashMap lookupMetadataFromIdentifier(String protocol, String authority, String identifier) { - logger.log(Level.FINE,"lookupMetadataFromIdentifier"); - String identifierOut = getIdentifierForLookup(protocol, authority, identifier); - HashMap metadata = new HashMap<>(); - try { - metadata = doiDataCiteRegisterService.getMetadata(identifierOut); - } catch (Exception e) { - logger.log(Level.WARNING, "None existing so we can use this identifier"); - logger.log(Level.WARNING, "identifier: {0}", identifierOut); - } - return metadata; - } - - /** * Modifies the DOI metadata for a Dataset * @param dvObject the dvObject whose metadata needs to be modified @@ -219,9 +187,9 @@ public void deleteIdentifier(DvObject dvObject) throws IOException, HttpExceptio private void deleteDraftIdentifier(DvObject dvObject) throws IOException { //ToDo - incorporate into DataCiteRESTfulClient - String baseUrl = systemConfig.getDataCiteRestApiUrlString(); - String username = System.getProperty("doi.username"); - String password = System.getProperty("doi.password"); + String baseUrl = JvmSettings.DATACITE_REST_API_URL.lookup(); + String username = JvmSettings.DATACITE_USERNAME.lookup(); + String password = JvmSettings.DATACITE_PASSWORD.lookup(); GlobalId doi = dvObject.getGlobalId(); /** * Deletes the DOI from DataCite if it can. Returns 204 if PID was deleted @@ -269,13 +237,13 @@ public boolean publicizeIdentifier(DvObject dvObject) { @Override public List getProviderInformation(){ - ArrayList providerInfo = new ArrayList<>(); - String providerName = "DataCite"; - String providerLink = "http://status.datacite.org"; - providerInfo.add(providerName); - providerInfo.add(providerLink); - return providerInfo; + return List.of("DataCite", "https://status.datacite.org"); } + + @Override + protected String getProviderKeyName() { + return "DataCite"; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/DOIEZIdServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DOIEZIdServiceBean.java index d21caf32411..d9b0fde15da 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DOIEZIdServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DOIEZIdServiceBean.java @@ -1,11 +1,12 @@ package edu.harvard.iq.dataverse; +import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.ucsb.nceas.ezid.EZIDException; import edu.ucsb.nceas.ezid.EZIDService; -import edu.ucsb.nceas.ezid.EZIDServiceRequest; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; + import javax.ejb.Stateless; /** @@ -13,32 +14,44 @@ * @author skraffmiller */ @Stateless -public class DOIEZIdServiceBean extends AbstractGlobalIdServiceBean { - +public class DOIEZIdServiceBean extends DOIServiceBean { + + private static final Logger logger = Logger.getLogger(DOIEZIdServiceBean.class.getCanonicalName()); + EZIDService ezidService; - EZIDServiceRequest ezidServiceRequest; - String baseURLString = "https://ezid.cdlib.org"; - private static final Logger logger = Logger.getLogger("edu.harvard.iq.dvn.core.index.DOIEZIdServiceBean"); - - // get username and password from system properties - private String USERNAME = ""; - private String PASSWORD = ""; - + + // This has a sane default in microprofile-config.properties + private final String baseUrl = JvmSettings.EZID_API_URL.lookup(); + public DOIEZIdServiceBean() { - logger.log(Level.FINE,"Constructor"); - baseURLString = System.getProperty("doi.baseurlstring"); - ezidService = new EZIDService(baseURLString); - USERNAME = System.getProperty("doi.username"); - PASSWORD = System.getProperty("doi.password"); - logger.log(Level.FINE, "Using baseURLString {0}", baseURLString); + // Creating the service doesn't do any harm, just initializing some object data here. + // Makes sure we don't run into NPEs from the other methods, but will obviously fail if the + // login below does not work. + this.ezidService = new EZIDService(this.baseUrl); + try { - ezidService.login(USERNAME, PASSWORD); + // These have (obviously) no default, but still are optional to make the provider optional + String username = JvmSettings.EZID_USERNAME.lookupOptional().orElse(null); + String password = JvmSettings.EZID_PASSWORD.lookupOptional().orElse(null); + + if (username != null ^ password != null) { + logger.log(Level.WARNING, "You must give both username and password. Will not try to login."); + } + + if (username != null && password != null) { + this.ezidService.login(username, password); + this.configured = true; + } } catch (EZIDException e) { - logger.log(Level.WARNING, "login failed "); + // We only do the warnings here, but the object still needs to be created. + // The EJB stateless thing expects this to go through, and it is requested on any + // global id parsing. + logger.log(Level.WARNING, "Login failed to {0}", this.baseUrl); logger.log(Level.WARNING, "Exception String: {0}", e.toString()); - logger.log(Level.WARNING, "localized message: {0}", e.getLocalizedMessage()); - logger.log(Level.WARNING, "cause: ", e.getCause()); - logger.log(Level.WARNING, "message {0}", e.getMessage()); + logger.log(Level.WARNING, "Localized message: {0}", e.getLocalizedMessage()); + logger.log(Level.WARNING, "Cause:", e.getCause()); + logger.log(Level.WARNING, "Message {0}", e.getMessage()); + // TODO: is this antipattern really necessary? } catch (Exception e) { logger.log(Level.SEVERE, "Other Error on ezidService.login(USERNAME, PASSWORD) - not EZIDException ", e.getMessage()); } @@ -50,19 +63,10 @@ public boolean registerWhenPublished() { } @Override - public boolean alreadyExists(DvObject dvObject) throws Exception { - if(dvObject==null) { - logger.severe("Null DvObject sent to alreadyExists()."); - return false; - } - return alreadyExists(dvObject.getGlobalId()); - } - - @Override - public boolean alreadyExists(GlobalId pid) throws Exception { - logger.log(Level.FINE,"alreadyExists"); + public boolean alreadyRegistered(GlobalId pid, boolean noProviderDefault) throws Exception { + logger.log(Level.FINE,"alreadyRegistered"); try { - HashMap result = ezidService.getMetadata(pid.asString()); + HashMap result = ezidService.getMetadata(pid.asString()); return result != null && !result.isEmpty(); // TODO just check for HTTP status code 200/404, sadly the status code is swept under the carpet } catch (EZIDException e ){ @@ -74,7 +78,7 @@ public boolean alreadyExists(GlobalId pid) throws Exception { if (e.getLocalizedMessage().contains("no such identifier")){ return false; } - logger.log(Level.WARNING, "alreadyExists failed"); + logger.log(Level.WARNING, "alreadyRegistered failed"); logger.log(Level.WARNING, "getIdentifier(dvObject) {0}", pid.asString()); logger.log(Level.WARNING, "String {0}", e.toString()); logger.log(Level.WARNING, "localized message {0}", e.getLocalizedMessage()); @@ -102,32 +106,6 @@ public Map getIdentifierMetadata(DvObject dvObject) { return metadata; } - /** - * Looks up the metadata for a Global Identifier - * - * @param protocol the identifier system, e.g. "doi" - * @param authority the namespace that the authority manages in the - * identifier system - * identifier part - * @param identifier the local identifier part - * @return a Map of metadata. It is empty when the lookup failed, e.g. when - * the identifier does not exist. - */ - @Override - public HashMap lookupMetadataFromIdentifier(String protocol, String authority, String identifier) { - logger.log(Level.FINE,"lookupMetadataFromIdentifier"); - String identifierOut = getIdentifierForLookup(protocol, authority, identifier); - HashMap metadata = new HashMap<>(); - try { - metadata = ezidService.getMetadata(identifierOut); - } catch (EZIDException e) { - logger.log(Level.FINE, "None existing so we can use this identifier"); - logger.log(Level.FINE, "identifier: {0}", identifierOut); - return metadata; - } - return metadata; - } - /** * Modifies the EZID metadata for a Dataset * @@ -249,12 +227,7 @@ private boolean updateIdentifierStatus(DvObject dvObject, String statusIn) { @Override public List getProviderInformation(){ - ArrayList providerInfo = new ArrayList<>(); - String providerName = "EZID"; - String providerLink = baseURLString; - providerInfo.add(providerName); - providerInfo.add(providerLink); - return providerInfo; + return List.of("EZID", this.baseUrl); } @Override @@ -301,5 +274,10 @@ private HashMap asHashMap(Map map) { return (map instanceof HashMap) ? (HashMap)map : new HashMap<>(map); } + @Override + protected String getProviderKeyName() { + return "EZID"; + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/DOIServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DOIServiceBean.java new file mode 100644 index 00000000000..0182c745cd0 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/DOIServiceBean.java @@ -0,0 +1,78 @@ +package edu.harvard.iq.dataverse; + +import edu.harvard.iq.dataverse.settings.SettingsServiceBean.Key; + +public abstract class DOIServiceBean extends AbstractGlobalIdServiceBean { + + public static final String DOI_PROTOCOL = "doi"; + public static final String DOI_RESOLVER_URL = "https://doi.org/"; + public static final String HTTP_DOI_RESOLVER_URL = "http://doi.org/"; + public static final String DXDOI_RESOLVER_URL = "https://dx.doi.org/"; + public static final String HTTP_DXDOI_RESOLVER_URL = "http://dx.doi.org/"; + + public DOIServiceBean() { + super(); + } + + @Override + public GlobalId parsePersistentId(String pidString) { + if (pidString.startsWith(DOI_RESOLVER_URL)) { + pidString = pidString.replace(DOI_RESOLVER_URL, + (DOI_PROTOCOL + ":")); + } else if (pidString.startsWith(HTTP_DOI_RESOLVER_URL)) { + pidString = pidString.replace(HTTP_DOI_RESOLVER_URL, + (DOI_PROTOCOL + ":")); + } else if (pidString.startsWith(DXDOI_RESOLVER_URL)) { + pidString = pidString.replace(DXDOI_RESOLVER_URL, + (DOI_PROTOCOL + ":")); + } + return super.parsePersistentId(pidString); + } + + @Override + public GlobalId parsePersistentId(String protocol, String identifierString) { + + if (!DOI_PROTOCOL.equals(protocol)) { + return null; + } + GlobalId globalId = super.parsePersistentId(protocol, identifierString); + if (globalId!=null && !GlobalIdServiceBean.checkDOIAuthority(globalId.getAuthority())) { + return null; + } + return globalId; + } + + @Override + public GlobalId parsePersistentId(String protocol, String authority, String identifier) { + + if (!DOI_PROTOCOL.equals(protocol)) { + return null; + } + return super.parsePersistentId(protocol, authority, identifier); + } + + public String getUrlPrefix() { + return DOI_RESOLVER_URL; + } + + @Override + public boolean isConfigured() { + if (configured == null) { + if (getProviderKeyName() == null) { + configured = false; + } else { + String doiProvider = settingsService.getValueForKey(Key.DoiProvider, ""); + if (getProviderKeyName().equals(doiProvider)) { + configured = true; + } else if (!doiProvider.isEmpty()) { + configured = false; + } + } + } + return super.isConfigured(); + } + + protected String getProviderKeyName() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/DataCitation.java b/src/main/java/edu/harvard/iq/dataverse/DataCitation.java index abe3cc3e6d7..30e03046822 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataCitation.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataCitation.java @@ -57,7 +57,7 @@ public class DataCitation { private String publisher; private boolean direct; private List funders; - private String seriesTitle; + private List seriesTitles; private String description; private List datesOfCollection; private List keywords; @@ -135,7 +135,7 @@ private void getCommonValuesFrom(DatasetVersion dsv) { datesOfCollection = dsv.getDatesOfCollection(); title = dsv.getTitle(); - seriesTitle = dsv.getSeriesTitle(); + seriesTitles = dsv.getSeriesTitles(); keywords = dsv.getKeywords(); languages = dsv.getLanguages(); spatialCoverages = dsv.getSpatialCoverages(); @@ -207,7 +207,7 @@ public String toString(boolean html, boolean anonymized) { if (persistentId != null) { // always show url format - citationList.add(formatURL(persistentId.toURL().toString(), persistentId.toURL().toString(), html)); + citationList.add(formatURL(persistentId.asURL(), persistentId.asURL(), html)); } citationList.add(formatString(publisher, html)); citationList.add(version); @@ -298,7 +298,7 @@ public void writeAsBibtexCitation(OutputStream os) throws IOException { out.write(persistentId.getIdentifier()); out.write("},\r\n"); out.write("url = {"); - out.write(persistentId.toURL().toString()); + out.write(persistentId.asURL()); out.write("}\r\n"); out.write("}\r\n"); out.flush(); @@ -330,8 +330,10 @@ public void writeAsRISCitation(OutputStream os) throws IOException { out.write("TY - DATA" + "\r\n"); out.write("T1 - " + getTitle() + "\r\n"); } - if (seriesTitle != null) { - out.write("T3 - " + seriesTitle + "\r\n"); + if (seriesTitles != null) { + for (String seriesTitle : seriesTitles) { + out.write("T3 - " + seriesTitle + "\r\n"); + } } /* Removing abstract/description per Request from G. King in #3759 if(description!=null) { @@ -387,7 +389,7 @@ public void writeAsRISCitation(OutputStream os) throws IOException { out.write("SE - " + date + "\r\n"); - out.write("UR - " + persistentId.toURL().toString() + "\r\n"); + out.write("UR - " + persistentId.asURL() + "\r\n"); out.write("PB - " + publisher + "\r\n"); // a DataFile citation also includes filename und UNF, if applicable: @@ -505,12 +507,22 @@ private void createEndNoteXML(XMLStreamWriter xmlw) throws XMLStreamException { xmlw.writeCharacters(title); xmlw.writeEndElement(); // title } - - if (seriesTitle != null) { - xmlw.writeStartElement("tertiary-title"); - xmlw.writeCharacters(seriesTitle); + + /* + If I say just !"isEmpty" for series titles I get a failure + on testToEndNoteString_withoutTitleAndAuthor + with a null pointer on build -SEK 3/31/23 + */ + if (seriesTitles != null && !seriesTitles.isEmpty() ) { + xmlw.writeStartElement("tertiary-titles"); + for (String seriesTitle : seriesTitles){ + xmlw.writeStartElement("tertiary-title"); + xmlw.writeCharacters(seriesTitle); + xmlw.writeEndElement(); // tertiary-title + } xmlw.writeEndElement(); // tertiary-title } + xmlw.writeEndElement(); // titles xmlw.writeStartElement("section"); @@ -584,7 +596,7 @@ private void createEndNoteXML(XMLStreamWriter xmlw) throws XMLStreamException { xmlw.writeStartElement("urls"); xmlw.writeStartElement("related-urls"); xmlw.writeStartElement("url"); - xmlw.writeCharacters(getPersistentId().toURL().toString()); + xmlw.writeCharacters(getPersistentId().asURL()); xmlw.writeEndElement(); // url xmlw.writeEndElement(); // related-urls xmlw.writeEndElement(); // urls @@ -781,18 +793,13 @@ private GlobalId getPIDFrom(DatasetVersion dsv, DvObject dv) { || HarvestingClient.HARVEST_STYLE_ICPSR.equals(dsv.getDataset().getHarvestedFrom().getHarvestStyle()) || HarvestingClient.HARVEST_STYLE_DATAVERSE .equals(dsv.getDataset().getHarvestedFrom().getHarvestStyle())) { - // creating a global id like this: - // persistentId = new GlobalId(dv.getGlobalId()); - // you end up doing new GlobalId((New GlobalId(dv)).toString()) - // - doing an extra formatting-and-parsing-again - // This achieves the same thing: if(!isDirect()) { if (!StringUtils.isEmpty(dsv.getDataset().getIdentifier())) { - return new GlobalId(dsv.getDataset()); + return dsv.getDataset().getGlobalId(); } } else { if (!StringUtils.isEmpty(dv.getIdentifier())) { - return new GlobalId(dv); + return dv.getGlobalId(); } } } diff --git a/src/main/java/edu/harvard/iq/dataverse/DataFile.java b/src/main/java/edu/harvard/iq/dataverse/DataFile.java index 5171e8d49f2..4e323496188 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataFile.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataFile.java @@ -5,12 +5,11 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; import edu.harvard.iq.dataverse.DatasetVersion.VersionState; +import edu.harvard.iq.dataverse.authorization.RoleAssignee; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.dataaccess.DataAccess; import edu.harvard.iq.dataverse.dataaccess.StorageIO; -import edu.harvard.iq.dataverse.dataset.DatasetThumbnail; import edu.harvard.iq.dataverse.datasetutility.FileSizeChecker; import edu.harvard.iq.dataverse.ingest.IngestReport; import edu.harvard.iq.dataverse.ingest.IngestRequest; @@ -19,17 +18,17 @@ import edu.harvard.iq.dataverse.util.ShapefileHandler; import edu.harvard.iq.dataverse.util.StringUtil; import java.io.IOException; +import java.util.Date; import java.util.List; import java.util.ArrayList; import java.util.Objects; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.Files; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.logging.Logger; +import java.util.stream.Collectors; import javax.json.Json; import javax.json.JsonArrayBuilder; import javax.persistence.*; @@ -47,9 +46,9 @@ query = "SELECT o FROM DataFile o WHERE o.creator.id=:creatorId"), @NamedQuery(name = "DataFile.findByReleaseUserId", query = "SELECT o FROM DataFile o WHERE o.releaseUser.id=:releaseUserId"), - @NamedQuery(name="DataFile.findDataFileByIdProtocolAuth", + @NamedQuery(name="DataFile.findDataFileByIdProtocolAuth", query="SELECT s FROM DataFile s WHERE s.identifier=:identifier AND s.protocol=:protocol AND s.authority=:authority"), - @NamedQuery(name="DataFile.findDataFileThatReplacedId", + @NamedQuery(name="DataFile.findDataFileThatReplacedId", query="SELECT s.id FROM DataFile s WHERE s.previousDataFileId=:identifier") }) @Entity @@ -73,7 +72,10 @@ public class DataFile extends DvObject implements Comparable { @Column( nullable = false ) @Pattern(regexp = "^.*/.*$", message = "{contenttype.slash}") private String contentType; - + + public void setFileAccessRequests(List fileAccessRequests) { + this.fileAccessRequests = fileAccessRequests; + } // @Expose // @SerializedName("storageIdentifier") @@ -416,7 +418,7 @@ public String getIngestReportMessage() { return ingestReports.get(0).getReport(); } } - return "Ingest failed. No further information is available."; + return BundleUtil.getStringFromBundle("file.ingestFailed"); } public boolean isTabularData() { @@ -747,22 +749,71 @@ public String getUnf() { } return null; } - - @ManyToMany - @JoinTable(name = "fileaccessrequests", - joinColumns = @JoinColumn(name = "datafile_id"), - inverseJoinColumns = @JoinColumn(name = "authenticated_user_id")) - private List fileAccessRequesters; + @OneToMany(mappedBy = "dataFile", cascade = {CascadeType.REMOVE, CascadeType.MERGE, CascadeType.PERSIST}, orphanRemoval = true) + private List fileAccessRequests; - public List getFileAccessRequesters() { - return fileAccessRequesters; + public List getFileAccessRequests() { + return fileAccessRequests; } - public void setFileAccessRequesters(List fileAccessRequesters) { - this.fileAccessRequesters = fileAccessRequesters; + public void addFileAccessRequester(AuthenticatedUser authenticatedUser) { + if (this.fileAccessRequests == null) { + this.fileAccessRequests = new ArrayList<>(); + } + + Set existingUsers = this.fileAccessRequests.stream() + .map(FileAccessRequest::getAuthenticatedUser) + .collect(Collectors.toSet()); + + if (existingUsers.contains(authenticatedUser)) { + return; + } + + FileAccessRequest request = new FileAccessRequest(); + request.setCreationTime(new Date()); + request.setDataFile(this); + request.setAuthenticatedUser(authenticatedUser); + + FileAccessRequest.FileAccessRequestKey key = new FileAccessRequest.FileAccessRequestKey(); + key.setAuthenticatedUser(authenticatedUser.getId()); + key.setDataFile(this.getId()); + + request.setId(key); + + this.fileAccessRequests.add(request); } - + + public boolean removeFileAccessRequester(RoleAssignee roleAssignee) { + if (this.fileAccessRequests == null) { + return false; + } + + FileAccessRequest request = this.fileAccessRequests.stream() + .filter(fileAccessRequest -> fileAccessRequest.getAuthenticatedUser().equals(roleAssignee)) + .findFirst() + .orElse(null); + + if (request != null) { + this.fileAccessRequests.remove(request); + return true; + } + + return false; + } + + public boolean containsFileAccessRequestFromUser(RoleAssignee roleAssignee) { + if (this.fileAccessRequests == null) { + return false; + } + + Set existingUsers = this.fileAccessRequests.stream() + .map(FileAccessRequest::getAuthenticatedUser) + .collect(Collectors.toSet()); + + return existingUsers.contains(roleAssignee); + } + public boolean isHarvested() { Dataset ownerDataset = this.getOwner(); @@ -956,7 +1007,7 @@ public JsonObject asGsonObject(boolean prettyPrint){ // https://github.com/IQSS/dataverse/issues/761, https://github.com/IQSS/dataverse/issues/2110, https://github.com/IQSS/dataverse/issues/3191 // datasetMap.put("title", thisFileMetadata.getDatasetVersion().getTitle()); - datasetMap.put("persistentId", getOwner().getGlobalIdString()); + datasetMap.put("persistentId", getOwner().getGlobalId().asString()); datasetMap.put("url", getOwner().getPersistentURL()); datasetMap.put("version", thisFileMetadata.getDatasetVersion().getSemanticVersion()); datasetMap.put("id", getOwner().getId()); @@ -1034,6 +1085,10 @@ public String getCreateDateFormattedYYYYMMDD() { return null; } + @Override + public String getTargetUrl() { + return DataFile.TARGET_URL; + } } // end of class diff --git a/src/main/java/edu/harvard/iq/dataverse/DataFileServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataFileServiceBean.java index 7da06f36be4..c30bfce368a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataFileServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataFileServiceBean.java @@ -1,7 +1,5 @@ package edu.harvard.iq.dataverse; -import edu.harvard.iq.dataverse.authorization.AccessRequest; -import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.dataaccess.DataAccess; import edu.harvard.iq.dataverse.dataaccess.ImageThumbConverter; import edu.harvard.iq.dataverse.dataaccess.StorageIO; @@ -11,19 +9,15 @@ import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.FileSortFieldAndOrder; import edu.harvard.iq.dataverse.util.FileUtil; -import edu.harvard.iq.dataverse.util.SystemConfig; import java.io.IOException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; @@ -36,9 +30,7 @@ import javax.persistence.NoResultException; import javax.persistence.PersistenceContext; import javax.persistence.Query; -import javax.persistence.StoredProcedureQuery; import javax.persistence.TypedQuery; -import org.apache.commons.lang3.RandomStringUtils; /** * @@ -73,7 +65,7 @@ public class DataFileServiceBean implements java.io.Serializable { // Assorted useful mime types: // 3rd-party and/or proprietary tabular data formasts that we know - // how to ingest: + // how to ingest: private static final String MIME_TYPE_STATA = "application/x-stata"; private static final String MIME_TYPE_STATA13 = "application/x-stata-13"; @@ -155,7 +147,7 @@ public DataFile find(Object pk) { }*/ public DataFile findByGlobalId(String globalId) { - return (DataFile) dvObjectService.findByGlobalId(globalId, DataFile.DATAFILE_DTYPE_STRING); + return (DataFile) dvObjectService.findByGlobalId(globalId, DvObject.DType.DataFile); } public List findByCreatorId(Long creatorId) { @@ -199,6 +191,18 @@ public List findByDatasetId(Long studyId) { .setParameter("studyId", studyId).getResultList(); } + /** + * + * @param collectionId numeric id of the parent collection ("dataverse") + * @return list of files in the datasets that are *direct* children of the collection specified + * (i.e., no datafiles in sub-collections of this collection will be included) + */ + public List findByDirectCollectionOwner(Long collectionId) { + String queryString = "select f from DataFile f, Dataset d where f.owner.id = d.id and d.owner.id = :collectionId order by f.id"; + return em.createQuery(queryString, DataFile.class) + .setParameter("collectionId", collectionId).getResultList(); + } + public List findAllRelatedByRootDatafileId(Long datafileId) { /* Get all files with the same root datafile id @@ -357,7 +361,7 @@ public DataFile findCheapAndEasy(Long id) { Object[] result; try { - result = (Object[]) em.createNativeQuery("SELECT t0.ID, t0.CREATEDATE, t0.INDEXTIME, t0.MODIFICATIONTIME, t0.PERMISSIONINDEXTIME, t0.PERMISSIONMODIFICATIONTIME, t0.PUBLICATIONDATE, t0.CREATOR_ID, t0.RELEASEUSER_ID, t0.PREVIEWIMAGEAVAILABLE, t1.CONTENTTYPE, t0.STORAGEIDENTIFIER, t1.FILESIZE, t1.INGESTSTATUS, t1.CHECKSUMVALUE, t1.RESTRICTED, t3.ID, t2.AUTHORITY, t2.IDENTIFIER, t1.CHECKSUMTYPE, t1.PREVIOUSDATAFILEID, t1.ROOTDATAFILEID, t0.AUTHORITY, T0.PROTOCOL, T0.IDENTIFIER FROM DVOBJECT t0, DATAFILE t1, DVOBJECT t2, DATASET t3 WHERE ((t0.ID = " + id + ") AND (t0.OWNER_ID = t2.ID) AND (t2.ID = t3.ID) AND (t1.ID = t0.ID))").getSingleResult(); + result = (Object[]) em.createNativeQuery("SELECT t0.ID, t0.CREATEDATE, t0.INDEXTIME, t0.MODIFICATIONTIME, t0.PERMISSIONINDEXTIME, t0.PERMISSIONMODIFICATIONTIME, t0.PUBLICATIONDATE, t0.CREATOR_ID, t0.RELEASEUSER_ID, t0.PREVIEWIMAGEAVAILABLE, t1.CONTENTTYPE, t0.STORAGEIDENTIFIER, t1.FILESIZE, t1.INGESTSTATUS, t1.CHECKSUMVALUE, t1.RESTRICTED, t3.ID, t2.AUTHORITY, t2.IDENTIFIER, t1.CHECKSUMTYPE, t1.PREVIOUSDATAFILEID, t1.ROOTDATAFILEID, t0.AUTHORITY, T0.PROTOCOL, T0.IDENTIFIER, t2.PROTOCOL FROM DVOBJECT t0, DATAFILE t1, DVOBJECT t2, DATASET t3 WHERE ((t0.ID = " + id + ") AND (t0.OWNER_ID = t2.ID) AND (t2.ID = t3.ID) AND (t1.ID = t0.ID))").getSingleResult(); } catch (Exception ex) { return null; } @@ -501,7 +505,9 @@ public DataFile findCheapAndEasy(Long id) { if (identifier != null) { dataFile.setIdentifier(identifier); } - + + owner.setProtocol((String) result[25]); + dataFile.setOwner(owner); // If content type indicates it's tabular data, spend 2 extra queries @@ -559,365 +565,6 @@ public DataFile findCheapAndEasy(Long id) { return dataFile; } - /* - * This is an experimental method for populating the versions of - * the datafile with the filemetadatas, optimized for making as few db - * queries as possible. - * It should only be used to retrieve filemetadata for the DatasetPage! - * It is not guaranteed to adequately perform anywhere else. - */ - - public void findFileMetadataOptimizedExperimental(Dataset owner, DatasetVersion version, AuthenticatedUser au) { - List dataFiles = new ArrayList<>(); - List dataTables = new ArrayList<>(); - //List retList = new ArrayList<>(); - - // TODO: - // replace these maps with simple lists and run binary search on them. -- 4.2.1 - - Map userMap = new HashMap<>(); - Map filesMap = new HashMap<>(); - Map datatableMap = new HashMap<>(); - Map categoryMap = new HashMap<>(); - Map> fileTagMap = new HashMap<>(); - List accessRequestFileIds = new ArrayList(); - - List fileTagLabels = DataFileTag.listTags(); - - - int i = 0; - //Cache responses - Map embargoMap = new HashMap(); - - List dataTableResults = em.createNativeQuery("SELECT t0.ID, t0.DATAFILE_ID, t0.UNF, t0.CASEQUANTITY, t0.VARQUANTITY, t0.ORIGINALFILEFORMAT, t0.ORIGINALFILESIZE, t0.ORIGINALFILENAME FROM dataTable t0, dataFile t1, dvObject t2 WHERE ((t0.DATAFILE_ID = t1.ID) AND (t1.ID = t2.ID) AND (t2.OWNER_ID = " + owner.getId() + ")) ORDER BY t0.ID").getResultList(); - - for (Object[] result : dataTableResults) { - DataTable dataTable = new DataTable(); - long fileId = ((Number) result[1]).longValue(); - - dataTable.setId(((Number) result[1]).longValue()); - - dataTable.setUnf((String)result[2]); - - dataTable.setCaseQuantity((Long)result[3]); - - dataTable.setVarQuantity((Long)result[4]); - - dataTable.setOriginalFileFormat((String)result[5]); - - dataTable.setOriginalFileSize((Long)result[6]); - - dataTable.setOriginalFileName((String)result[7]); - - dataTables.add(dataTable); - datatableMap.put(fileId, i++); - - } - - logger.fine("Retrieved "+dataTables.size()+" DataTable objects."); - - List dataTagsResults = em.createNativeQuery("SELECT t0.DATAFILE_ID, t0.TYPE FROM DataFileTag t0, dvObject t1 WHERE (t1.ID = t0.DATAFILE_ID) AND (t1.OWNER_ID="+ owner.getId() + ")").getResultList(); - for (Object[] result : dataTagsResults) { - Long datafile_id = (Long) result[0]; - Integer tagtype_id = (Integer) result[1]; - if (fileTagMap.get(datafile_id) == null) { - fileTagMap.put(datafile_id, new HashSet<>()); - } - fileTagMap.get(datafile_id).add(tagtype_id); - } - logger.fine("Retrieved "+dataTagsResults.size()+" data tags."); - dataTagsResults = null; - - //Only need to check for access requests if there is an authenticated user - if (au != null) { - List accessRequests = em.createNativeQuery("SELECT t0.ID FROM DVOBJECT t0, FILEACCESSREQUESTS t1 WHERE t1.datafile_id = t0.id and t0.OWNER_ID = " + owner.getId() + " and t1.AUTHENTICATED_USER_ID = " + au.getId() + " ORDER BY t0.ID").getResultList(); - for (Object result : accessRequests) { - accessRequestFileIds.add(Long.valueOf((Integer)result)); - } - logger.fine("Retrieved " + accessRequests.size() + " access requests."); - accessRequests = null; - } - - i = 0; - - List fileResults = em.createNativeQuery("SELECT t0.ID, t0.CREATEDATE, t0.INDEXTIME, t0.MODIFICATIONTIME, t0.PERMISSIONINDEXTIME, t0.PERMISSIONMODIFICATIONTIME, t0.PUBLICATIONDATE, t0.CREATOR_ID, t0.RELEASEUSER_ID, t1.CONTENTTYPE, t0.STORAGEIDENTIFIER, t1.FILESIZE, t1.INGESTSTATUS, t1.CHECKSUMVALUE, t1.RESTRICTED, t1.CHECKSUMTYPE, t1.PREVIOUSDATAFILEID, t1.ROOTDATAFILEID, t0.PROTOCOL, t0.AUTHORITY, t0.IDENTIFIER, t1.EMBARGO_ID FROM DVOBJECT t0, DATAFILE t1 WHERE ((t0.OWNER_ID = " + owner.getId() + ") AND ((t1.ID = t0.ID) AND (t0.DTYPE = 'DataFile'))) ORDER BY t0.ID").getResultList(); - - for (Object[] result : fileResults) { - Integer file_id = (Integer) result[0]; - - DataFile dataFile = new DataFile(); - dataFile.setMergeable(false); - - dataFile.setId(file_id.longValue()); - - Timestamp createDate = (Timestamp) result[1]; - Timestamp indexTime = (Timestamp) result[2]; - Timestamp modificationTime = (Timestamp) result[3]; - Timestamp permissionIndexTime = (Timestamp) result[4]; - Timestamp permissionModificationTime = (Timestamp) result[5]; - Timestamp publicationDate = (Timestamp) result[6]; - - dataFile.setCreateDate(createDate); - dataFile.setIndexTime(indexTime); - dataFile.setModificationTime(modificationTime); - dataFile.setPermissionIndexTime(permissionIndexTime); - dataFile.setPermissionModificationTime(permissionModificationTime); - dataFile.setPublicationDate(publicationDate); - - Long creatorId = (Long) result[7]; - if (creatorId != null) { - AuthenticatedUser creator = userMap.get(creatorId); - if (creator == null) { - creator = userService.find(creatorId); - if (creator != null) { - userMap.put(creatorId, creator); - } - } - if (creator != null) { - dataFile.setCreator(creator); - } - } - - dataFile.setOwner(owner); - - Long releaseUserId = (Long) result[8]; - if (releaseUserId != null) { - AuthenticatedUser releaseUser = userMap.get(releaseUserId); - if (releaseUser == null) { - releaseUser = userService.find(releaseUserId); - if (releaseUser != null) { - userMap.put(releaseUserId, releaseUser); - } - } - if (releaseUser != null) { - dataFile.setReleaseUser(releaseUser); - } - } - - String contentType = (String) result[9]; - - if (contentType != null) { - dataFile.setContentType(contentType); - } - - String storageIdentifier = (String) result[10]; - - if (storageIdentifier != null) { - dataFile.setStorageIdentifier(storageIdentifier); - } - - Long fileSize = (Long) result[11]; - - if (fileSize != null) { - dataFile.setFilesize(fileSize); - } - - if (result[12] != null) { - String ingestStatusString = (String) result[12]; - dataFile.setIngestStatus(ingestStatusString.charAt(0)); - } - - String md5 = (String) result[13]; - - if (md5 != null) { - dataFile.setChecksumValue(md5); - } - - Boolean restricted = (Boolean) result[14]; - if (restricted != null) { - dataFile.setRestricted(restricted); - } - - String checksumType = (String) result[15]; - if (checksumType != null) { - try { - // In the database we store "SHA1" rather than "SHA-1". - DataFile.ChecksumType typeFromStringInDatabase = DataFile.ChecksumType.valueOf(checksumType); - dataFile.setChecksumType(typeFromStringInDatabase); - } catch (IllegalArgumentException ex) { - logger.info("Exception trying to convert " + checksumType + " to enum: " + ex); - } - } - - Long previousDataFileId = (Long) result[16]; - if (previousDataFileId != null) { - dataFile.setPreviousDataFileId(previousDataFileId); - } - - Long rootDataFileId = (Long) result[17]; - if (rootDataFileId != null) { - dataFile.setRootDataFileId(rootDataFileId); - } - - String protocol = (String) result[18]; - if (protocol != null) { - dataFile.setProtocol(protocol); - } - - String authority = (String) result[19]; - if (authority != null) { - dataFile.setAuthority(authority); - } - - String identifier = (String) result[20]; - if (identifier != null) { - dataFile.setIdentifier(identifier); - } - - Long embargo_id = (Long) result[21]; - if (embargo_id != null) { - if (embargoMap.containsKey(embargo_id)) { - dataFile.setEmbargo(embargoMap.get(embargo_id)); - } else { - Embargo e = embargoService.findByEmbargoId(embargo_id); - dataFile.setEmbargo(e); - embargoMap.put(embargo_id, e); - } - } - - // TODO: - // - if ingest status is "bad", look up the ingest report; - // - is it a dedicated thumbnail for the dataset? (do we ever need that info?? - not on the dataset page, I don't think...) - - // Is this a tabular file? - - if (datatableMap.get(dataFile.getId()) != null) { - dataTables.get(datatableMap.get(dataFile.getId())).setDataFile(dataFile); - dataFile.setDataTable(dataTables.get(datatableMap.get(dataFile.getId()))); - - } - - if (fileTagMap.get(dataFile.getId()) != null) { - for (Integer tag_id : fileTagMap.get(dataFile.getId())) { - DataFileTag tag = new DataFileTag(); - tag.setTypeByLabel(fileTagLabels.get(tag_id)); - tag.setDataFile(dataFile); - dataFile.addTag(tag); - } - } - - if (dataFile.isRestricted() && accessRequestFileIds.contains(dataFile.getId())) { - dataFile.setFileAccessRequesters(Collections.singletonList(au)); - } - - dataFiles.add(dataFile); - filesMap.put(dataFile.getId(), i++); - } - fileResults = null; - - logger.fine("Retrieved and cached "+i+" datafiles."); - - i = 0; - for (DataFileCategory fileCategory : owner.getCategories()) { - //logger.fine("category: id="+fileCategory.getId()); - categoryMap.put(fileCategory.getId(), i++); - } - - logger.fine("Retrieved "+i+" file categories attached to the dataset."); - - version.setFileMetadatas(retrieveFileMetadataForVersion(owner, version, dataFiles, filesMap, categoryMap)); - logger.fine("Retrieved " + version.getFileMetadatas().size() + " filemetadatas for the version " + version.getId()); - owner.setFiles(dataFiles); - } - - private List retrieveFileMetadataForVersion(Dataset dataset, DatasetVersion version, List dataFiles, Map filesMap, Map categoryMap) { - List retList = new ArrayList<>(); - Map> categoryMetaMap = new HashMap<>(); - - List categoryResults = em.createNativeQuery("select t0.filecategories_id, t0.filemetadatas_id from filemetadata_datafilecategory t0, filemetadata t1 where (t0.filemetadatas_id = t1.id) AND (t1.datasetversion_id = "+version.getId()+")").getResultList(); - int i = 0; - for (Object[] result : categoryResults) { - Long category_id = (Long) result[0]; - Long filemeta_id = (Long) result[1]; - if (categoryMetaMap.get(filemeta_id) == null) { - categoryMetaMap.put(filemeta_id, new HashSet<>()); - } - categoryMetaMap.get(filemeta_id).add(category_id); - i++; - } - logger.fine("Retrieved and mapped "+i+" file categories attached to files in the version "+version.getId()); - - List metadataResults = em.createNativeQuery("select id, datafile_id, DESCRIPTION, LABEL, RESTRICTED, DIRECTORYLABEL, prov_freeform from FileMetadata where datasetversion_id = "+version.getId() + " ORDER BY LABEL").getResultList(); - - for (Object[] result : metadataResults) { - Integer filemeta_id = (Integer) result[0]; - - if (filemeta_id == null) { - continue; - } - - Long file_id = (Long) result[1]; - if (file_id == null) { - continue; - } - - Integer file_list_id = filesMap.get(file_id); - if (file_list_id == null) { - continue; - } - FileMetadata fileMetadata = new FileMetadata(); - fileMetadata.setId(filemeta_id.longValue()); - fileMetadata.setCategories(new LinkedList<>()); - - if (categoryMetaMap.get(fileMetadata.getId()) != null) { - for (Long cat_id : categoryMetaMap.get(fileMetadata.getId())) { - if (categoryMap.get(cat_id) != null) { - fileMetadata.getCategories().add(dataset.getCategories().get(categoryMap.get(cat_id))); - } - } - } - - fileMetadata.setDatasetVersion(version); - - // Link the FileMetadata object to the DataFile: - fileMetadata.setDataFile(dataFiles.get(file_list_id)); - // ... and the DataFile back to the FileMetadata: - fileMetadata.getDataFile().getFileMetadatas().add(fileMetadata); - - String description = (String) result[2]; - - if (description != null) { - fileMetadata.setDescription(description); - } - - String label = (String) result[3]; - - if (label != null) { - fileMetadata.setLabel(label); - } - - Boolean restricted = (Boolean) result[4]; - if (restricted != null) { - fileMetadata.setRestricted(restricted); - } - - String dirLabel = (String) result[5]; - if (dirLabel != null){ - fileMetadata.setDirectoryLabel(dirLabel); - } - - String provFreeForm = (String) result[6]; - if (provFreeForm != null){ - fileMetadata.setProvFreeForm(provFreeForm); - } - - retList.add(fileMetadata); - } - - logger.fine("Retrieved "+retList.size()+" file metadatas for version "+version.getId()+" (inside the retrieveFileMetadataForVersion method)."); - - - /* - We no longer perform this sort here, just to keep this filemetadata - list as identical as possible to when it's produced by the "traditional" - EJB method. When it's necessary to have the filemetadatas sorted by - FileMetadata.compareByLabel, the DatasetVersion.getFileMetadatasSorted() - method should be called. - - Collections.sort(retList, FileMetadata.compareByLabel); */ - - return retList; - } public List findIngestsInProgress() { if ( em.isOpen() ) { @@ -1427,75 +1074,6 @@ public List selectFilesWithMissingOriginalSizes() { } } - public String generateDataFileIdentifier(DataFile datafile, GlobalIdServiceBean idServiceBean) { - String doiIdentifierType = settingsService.getValueForKey(SettingsServiceBean.Key.IdentifierGenerationStyle, "randomString"); - String doiDataFileFormat = settingsService.getValueForKey(SettingsServiceBean.Key.DataFilePIDFormat, "DEPENDENT"); - - String prepend = ""; - if (doiDataFileFormat.equals(SystemConfig.DataFilePIDFormat.DEPENDENT.toString())){ - //If format is dependent then pre-pend the dataset identifier - prepend = datafile.getOwner().getIdentifier() + "/"; - } else { - //If there's a shoulder prepend independent identifiers with it - prepend = settingsService.getValueForKey(SettingsServiceBean.Key.Shoulder, ""); - } - - switch (doiIdentifierType) { - case "randomString": - return generateIdentifierAsRandomString(datafile, idServiceBean, prepend); - case "storedProcGenerated": - if (doiDataFileFormat.equals(SystemConfig.DataFilePIDFormat.INDEPENDENT.toString())){ - return generateIdentifierFromStoredProcedureIndependent(datafile, idServiceBean, prepend); - } else { - return generateIdentifierFromStoredProcedureDependent(datafile, idServiceBean, prepend); - } - default: - /* Should we throw an exception instead?? -- L.A. 4.6.2 */ - return generateIdentifierAsRandomString(datafile, idServiceBean, prepend); - } - } - - private String generateIdentifierAsRandomString(DataFile datafile, GlobalIdServiceBean idServiceBean, String prepend) { - String identifier = null; - do { - identifier = prepend + RandomStringUtils.randomAlphanumeric(6).toUpperCase(); - } while (!isGlobalIdUnique(identifier, datafile, idServiceBean)); - - return identifier; - } - - - private String generateIdentifierFromStoredProcedureIndependent(DataFile datafile, GlobalIdServiceBean idServiceBean, String prepend) { - String identifier; - do { - StoredProcedureQuery query = this.em.createNamedStoredProcedureQuery("Dataset.generateIdentifierFromStoredProcedure"); - query.execute(); - String identifierFromStoredProcedure = (String) query.getOutputParameterValue(1); - // some diagnostics here maybe - is it possible to determine that it's failing - // because the stored procedure hasn't been created in the database? - if (identifierFromStoredProcedure == null) { - return null; - } - identifier = prepend + identifierFromStoredProcedure; - } while (!isGlobalIdUnique(identifier, datafile, idServiceBean)); - - return identifier; - } - - private String generateIdentifierFromStoredProcedureDependent(DataFile datafile, GlobalIdServiceBean idServiceBean, String prepend) { - String identifier; - Long retVal; - - retVal = new Long(0); - - do { - retVal++; - identifier = prepend + retVal.toString(); - - } while (!isGlobalIdUnique(identifier, datafile, idServiceBean)); - - return identifier; - } /** * Check that a identifier entered by the user is unique (not currently used @@ -1506,38 +1084,6 @@ private String generateIdentifierFromStoredProcedureDependent(DataFile datafile, * @param idServiceBean * @return {@code true} iff the global identifier is unique. */ - public boolean isGlobalIdUnique(String userIdentifier, DataFile datafile, GlobalIdServiceBean idServiceBean) { - String testProtocol = ""; - String testAuthority = ""; - if (datafile.getAuthority() != null){ - testAuthority = datafile.getAuthority(); - } else { - testAuthority = settingsService.getValueForKey(SettingsServiceBean.Key.Authority); - } - if (datafile.getProtocol() != null){ - testProtocol = datafile.getProtocol(); - } else { - testProtocol = settingsService.getValueForKey(SettingsServiceBean.Key.Protocol); - } - - boolean u = em.createNamedQuery("DvObject.findByProtocolIdentifierAuthority") - .setParameter("protocol", testProtocol) - .setParameter("authority", testAuthority) - .setParameter("identifier",userIdentifier) - .getResultList().isEmpty(); - - try{ - if (idServiceBean.alreadyExists(new GlobalId(testProtocol, testAuthority, userIdentifier))) { - u = false; - } - } catch (Exception e){ - //we can live with failure - means identifier not found remotely - } - - - return u; - } - public void finalizeFileDelete(Long dataFileId, String storageLocation) throws IOException { // Verify that the DataFile no longer exists: if (find(dataFileId) != null) { diff --git a/src/main/java/edu/harvard/iq/dataverse/Dataset.java b/src/main/java/edu/harvard/iq/dataverse/Dataset.java index d7e7271738d..f9c839a0fff 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Dataset.java +++ b/src/main/java/edu/harvard/iq/dataverse/Dataset.java @@ -43,6 +43,10 @@ * @author skraffmiller */ @NamedQueries({ + // Dataset.findById should only be used if you're going to iterate over files (otherwise, lazy loading in DatasetService.find() is better). + // If you are going to iterate over files, preferably call the DatasetService.findDeep() method i.s.o. using this query directly. + @NamedQuery(name = "Dataset.findById", + query = "SELECT o FROM Dataset o LEFT JOIN FETCH o.files WHERE o.id=:id"), @NamedQuery(name = "Dataset.findIdStale", query = "SELECT d.id FROM Dataset d WHERE d.indexTime is NULL OR d.indexTime < d.modificationTime"), @NamedQuery(name = "Dataset.findIdStalePermission", @@ -258,7 +262,7 @@ public void setFileAccessRequest(boolean fileAccessRequest) { } public String getPersistentURL() { - return new GlobalId(this).toURL().toString(); + return this.getGlobalId().asURL(); } public List getFiles() { @@ -765,13 +769,13 @@ public String getLocalURL() { public String getRemoteArchiveURL() { if (isHarvested()) { if (HarvestingClient.HARVEST_STYLE_DATAVERSE.equals(this.getHarvestedFrom().getHarvestStyle())) { - return this.getHarvestedFrom().getArchiveUrl() + "/dataset.xhtml?persistentId=" + getGlobalIdString(); + return this.getHarvestedFrom().getArchiveUrl() + "/dataset.xhtml?persistentId=" + getGlobalId().asString(); } else if (HarvestingClient.HARVEST_STYLE_VDC.equals(this.getHarvestedFrom().getHarvestStyle())) { String rootArchiveUrl = this.getHarvestedFrom().getHarvestingUrl(); int c = rootArchiveUrl.indexOf("/OAIHandler"); if (c > 0) { rootArchiveUrl = rootArchiveUrl.substring(0, c); - return rootArchiveUrl + "/faces/study/StudyPage.xhtml?globalId=" + getGlobalIdString(); + return rootArchiveUrl + "/faces/study/StudyPage.xhtml?globalId=" + getGlobalId().asString(); } } else if (HarvestingClient.HARVEST_STYLE_ICPSR.equals(this.getHarvestedFrom().getHarvestStyle())) { // For the ICPSR, it turns out that the best thing to do is to @@ -881,7 +885,12 @@ public T accept(Visitor v) { @Override public String getDisplayName() { DatasetVersion dsv = getReleasedVersion(); - return dsv != null ? dsv.getTitle() : getLatestVersion().getTitle(); + String result = dsv != null ? dsv.getTitle() : getLatestVersion().getTitle(); + boolean resultIsEmpty = result == null || "".equals(result); + if (resultIsEmpty && getGlobalId() != null) { + return getGlobalId().asString(); + } + return result; } @Override @@ -915,4 +924,8 @@ public DatasetThumbnail getDatasetThumbnail(DatasetVersion datasetVersion, int s return DatasetUtil.getThumbnail(this, datasetVersion, size); } + @Override + public String getTargetUrl() { + return Dataset.TARGET_URL; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetFieldConstant.java b/src/main/java/edu/harvard/iq/dataverse/DatasetFieldConstant.java index 6d26c0cba58..e57a2a1538d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetFieldConstant.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetFieldConstant.java @@ -112,8 +112,8 @@ public class DatasetFieldConstant implements java.io.Serializable { public final static String geographicUnit="geographicUnit"; public final static String westLongitude="westLongitude"; public final static String eastLongitude="eastLongitude"; - public final static String northLatitude="northLongitude"; //Changed to match DB - incorrectly entered into DB - public final static String southLatitude="southLongitude"; //Incorrect in DB + public final static String northLatitude="northLongitude"; //Changed to match DB - incorrectly entered into DB: https://github.com/IQSS/dataverse/issues/5645 + public final static String southLatitude="southLongitude"; //Incorrect in DB: https://github.com/IQSS/dataverse/issues/5645 public final static String unitOfAnalysis="unitOfAnalysis"; public final static String universe="universe"; public final static String kindOfData="kindOfData"; diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetFieldServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetFieldServiceBean.java index 9bc5a5c09a7..89f8c11d076 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetFieldServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetFieldServiceBean.java @@ -22,12 +22,14 @@ import javax.inject.Named; import javax.json.Json; import javax.json.JsonArray; +import javax.json.JsonArrayBuilder; import javax.json.JsonException; import javax.json.JsonObject; import javax.json.JsonObjectBuilder; import javax.json.JsonReader; import javax.json.JsonString; import javax.json.JsonValue; +import javax.json.JsonValue.ValueType; import javax.persistence.EntityManager; import javax.persistence.NoResultException; import javax.persistence.NonUniqueResultException; @@ -343,33 +345,33 @@ public Map getCVocConf(boolean byTermUriField){ public void registerExternalVocabValues(DatasetField df) { DatasetFieldType dft =df.getDatasetFieldType(); logger.fine("Registering for field: " + dft.getName()); - JsonObject cvocEntry = getCVocConf(false).get(dft.getId()); - if(dft.isPrimitive()) { - for(DatasetFieldValue dfv: df.getDatasetFieldValues()) { + JsonObject cvocEntry = getCVocConf(true).get(dft.getId()); + if (dft.isPrimitive()) { + for (DatasetFieldValue dfv : df.getDatasetFieldValues()) { registerExternalTerm(cvocEntry, dfv.getValue()); } - } else { - if (df.getDatasetFieldType().isCompound()) { - DatasetFieldType termdft = findByNameOpt(cvocEntry.getString("term-uri-field")); - for (DatasetFieldCompoundValue cv : df.getDatasetFieldCompoundValues()) { - for (DatasetField cdf : cv.getChildDatasetFields()) { - logger.fine("Found term uri field type id: " + cdf.getDatasetFieldType().getId()); - if(cdf.getDatasetFieldType().equals(termdft)) { - registerExternalTerm(cvocEntry, cdf.getValue()); - } + } else { + if (df.getDatasetFieldType().isCompound()) { + DatasetFieldType termdft = findByNameOpt(cvocEntry.getString("term-uri-field")); + for (DatasetFieldCompoundValue cv : df.getDatasetFieldCompoundValues()) { + for (DatasetField cdf : cv.getChildDatasetFields()) { + logger.fine("Found term uri field type id: " + cdf.getDatasetFieldType().getId()); + if (cdf.getDatasetFieldType().equals(termdft)) { + registerExternalTerm(cvocEntry, cdf.getValue()); } } } } + } } /** * Retrieves indexable strings from a cached externalvocabularyvalue entry. * * This method assumes externalvocabularyvalue entries have been filtered and - * the externalvocabularyvalue entry contain a single JsonObject whose values - * are either Strings or an array of objects with "lang" and "value" keys. The - * string, or the "value"s for each language are added to the set. + * the externalvocabularyvalue entry contain a single JsonObject whose "personName" or "termName" values + * are either Strings or an array of objects with "lang" and ("value" or "content") keys. The + * string, or the "value/content"s for each language are added to the set. * * Any parsing error results in no entries (there can be unfiltered entries with * unknown structure - getting some strings from such an entry could give fairly @@ -385,16 +387,25 @@ public Set getStringsFor(String termUri) { if (jo != null) { try { for (String key : jo.keySet()) { - JsonValue jv = jo.get(key); - if (jv.getValueType().equals(JsonValue.ValueType.STRING)) { - logger.fine("adding " + jo.getString(key) + " for " + termUri); - strings.add(jo.getString(key)); - } else { - if (jv.getValueType().equals(JsonValue.ValueType.ARRAY)) { - JsonArray jarr = jv.asJsonArray(); - for (int i = 0; i < jarr.size(); i++) { - logger.fine("adding " + jarr.getJsonObject(i).getString("value") + " for " + termUri); - strings.add(jarr.getJsonObject(i).getString("value")); + if (key.equals("termName") || key.equals("personName")) { + JsonValue jv = jo.get(key); + if (jv.getValueType().equals(JsonValue.ValueType.STRING)) { + logger.fine("adding " + jo.getString(key) + " for " + termUri); + strings.add(jo.getString(key)); + } else { + if (jv.getValueType().equals(JsonValue.ValueType.ARRAY)) { + JsonArray jarr = jv.asJsonArray(); + for (int i = 0; i < jarr.size(); i++) { + JsonObject entry = jarr.getJsonObject(i); + if (entry.containsKey("value")) { + logger.fine("adding " + entry.getString("value") + " for " + termUri); + strings.add(entry.getString("value")); + } else if (entry.containsKey("content")) { + logger.fine("adding " + entry.getString("content") + " for " + termUri); + strings.add(entry.getString("content")); + + } + } } } } @@ -410,7 +421,7 @@ public Set getStringsFor(String termUri) { } /** - * Perform a query to retrieve a cached valie from the externalvocabularvalue table + * Perform a query to retrieve a cached value from the externalvocabularvalue table * @param termUri * @return - the entry's value as a JsonObject */ @@ -444,9 +455,25 @@ public void registerExternalTerm(JsonObject cvocEntry, String term) { logger.fine("Ingoring blank term"); return; } + boolean isExternal = false; + JsonObject vocabs = cvocEntry.getJsonObject("vocabs"); + for (String key: vocabs.keySet()) { + JsonObject vocab = vocabs.getJsonObject(key); + if (vocab.containsKey("uriSpace")) { + if (term.startsWith(vocab.getString("uriSpace"))) { + isExternal = true; + break; + } + } + } + if (!isExternal) { + logger.fine("Ignoring free text entry: " + term); + return; + } logger.fine("Registering term: " + term); try { - URI uri = new URI(term); + //Assure the term is in URI form - should be if the uriSpace entry was correct + new URI(term); ExternalVocabularyValue evv = null; try { evv = em.createQuery("select object(o) from ExternalVocabularyValue as o where o.uri=:uri", @@ -542,37 +569,7 @@ private JsonObject filterResponse(JsonObject cvocEntry, JsonObject readObject, S String[] pathParts = param.split("/"); logger.fine("PP: " + String.join(", ", pathParts)); JsonValue curPath = readObject; - for (int j = 0; j < pathParts.length - 1; j++) { - if (pathParts[j].contains("=")) { - JsonArray arr = ((JsonArray) curPath); - for (int k = 0; k < arr.size(); k++) { - String[] keyVal = pathParts[j].split("="); - logger.fine("Looking for object where " + keyVal[0] + " is " + keyVal[1]); - JsonObject jo = arr.getJsonObject(k); - String val = jo.getString(keyVal[0]); - String expected = keyVal[1]; - if (expected.equals("@id")) { - expected = termUri; - } - if (val.equals(expected)) { - logger.fine("Found: " + jo.toString()); - curPath = jo; - break; - } - } - } else { - curPath = ((JsonObject) curPath).get(pathParts[j]); - logger.fine("Found next Path object " + curPath.toString()); - } - } - JsonValue jv = ((JsonObject) curPath).get(pathParts[pathParts.length - 1]); - if (jv.getValueType().equals(JsonValue.ValueType.STRING)) { - vals.add(i, ((JsonString) jv).getString()); - } else if (jv.getValueType().equals(JsonValue.ValueType.ARRAY)) { - vals.add(i, jv); - } else if (jv.getValueType().equals(JsonValue.ValueType.OBJECT)) { - vals.add(i, jv); - } + vals.add(i, processPathSegment(0, pathParts, curPath, termUri)); logger.fine("Added param value: " + i + ": " + vals.get(i)); } else { logger.fine("Param is: " + param); @@ -615,6 +612,7 @@ private JsonObject filterResponse(JsonObject cvocEntry, JsonObject readObject, S } catch (Exception e) { logger.warning("External Vocabulary: " + termUri + " - Failed to find value for " + filterKey + ": " + e.getMessage()); + e.printStackTrace(); } } } @@ -628,6 +626,66 @@ private JsonObject filterResponse(JsonObject cvocEntry, JsonObject readObject, S } } + Object processPathSegment(int index, String[] pathParts, JsonValue curPath, String termUri) { + if (index < pathParts.length - 1) { + if (pathParts[index].contains("=")) { + JsonArray arr = ((JsonArray) curPath); + String[] keyVal = pathParts[index].split("="); + logger.fine("Looking for object where " + keyVal[0] + " is " + keyVal[1]); + String expected = keyVal[1]; + + if (!expected.equals("*")) { + if (expected.equals("@id")) { + expected = termUri; + } + for (int k = 0; k < arr.size(); k++) { + JsonObject jo = arr.getJsonObject(k); + String val = jo.getString(keyVal[0]); + if (val.equals(expected)) { + logger.fine("Found: " + jo.toString()); + curPath = jo; + return processPathSegment(index + 1, pathParts, curPath, termUri); + } + } + } else { + JsonArrayBuilder parts = Json.createArrayBuilder(); + for (JsonValue subPath : arr) { + if (subPath instanceof JsonObject) { + JsonValue nextValue = ((JsonObject) subPath).get(keyVal[0]); + Object obj = processPathSegment(index + 1, pathParts, nextValue, termUri); + if (obj instanceof String) { + parts.add((String) obj); + } else { + parts.add((JsonValue) obj); + } + } + } + return parts.build(); + } + + } else { + curPath = ((JsonObject) curPath).get(pathParts[index]); + logger.fine("Found next Path object " + curPath.toString()); + return processPathSegment(index + 1, pathParts, curPath, termUri); + } + } else { + logger.fine("Last segment: " + curPath.toString()); + logger.fine("Looking for : " + pathParts[index]); + JsonValue jv = ((JsonObject) curPath).get(pathParts[index]); + ValueType type =jv.getValueType(); + if (type.equals(JsonValue.ValueType.STRING)) { + return ((JsonString) jv).getString(); + } else if (jv.getValueType().equals(JsonValue.ValueType.ARRAY)) { + return jv; + } else if (jv.getValueType().equals(JsonValue.ValueType.OBJECT)) { + return jv; + } + } + + return null; + + } + /** * Supports validation of externally controlled values. If the value is a URI it * must be in the namespace (start with) one of the uriSpace values of an @@ -669,8 +727,20 @@ public boolean isValidCVocValue(DatasetFieldType dft, String value) { public List getVocabScripts( Map cvocConf) { //ToDo - only return scripts that are needed (those fields are set on display pages, those blocks/fields are allowed in the Dataverse collection for create/edit)? Set scripts = new HashSet(); - for(JsonObject jo: cvocConf.values()) { - scripts.add(jo.getString("js-url")); + for (JsonObject jo : cvocConf.values()) { + // Allow either a single script (a string) or an array of scripts (used, for + // example, to allow use of the common cvocutils.js script along with a main + // script for the field.) + JsonValue scriptValue = jo.get("js-url"); + ValueType scriptType = scriptValue.getValueType(); + if (scriptType.equals(ValueType.STRING)) { + scripts.add(((JsonString) scriptValue).getString()); + } else if (scriptType.equals(ValueType.ARRAY)) { + JsonArray scriptArray = ((JsonArray) scriptValue); + for (int i = 0; i < scriptArray.size(); i++) { + scripts.add(scriptArray.getString(i)); + } + } } String customScript = settingsService.getValueForKey(SettingsServiceBean.Key.ControlledVocabularyCustomJavaScript); if (customScript != null && !customScript.isEmpty()) { diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetFieldValueValidator.java b/src/main/java/edu/harvard/iq/dataverse/DatasetFieldValueValidator.java index 8b807f78bca..132955859ff 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetFieldValueValidator.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetFieldValueValidator.java @@ -16,6 +16,7 @@ import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; +import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.validation.EMailValidator; import edu.harvard.iq.dataverse.validation.URLValidator; import org.apache.commons.lang3.StringUtils; @@ -59,7 +60,7 @@ public boolean isValid(DatasetFieldValue value, ConstraintValidatorContext conte boolean valid = value.getValue().matches(value.getDatasetField().getDatasetFieldType().getValidationFormat()); if (!valid) { try { - context.buildConstraintViolationWithTemplate(dsfType.getDisplayName() + " is not a valid entry.").addConstraintViolation(); + context.buildConstraintViolationWithTemplate(dsfType.getDisplayName() + " " + BundleUtil.getStringFromBundle("dataset.metadata.invalidEntry")).addConstraintViolation(); } catch (NullPointerException e) { return false; } @@ -128,7 +129,7 @@ public boolean isValid(DatasetFieldValue value, ConstraintValidatorContext conte } if (!valid) { try { - context.buildConstraintViolationWithTemplate(dsfType.getDisplayName() + " is not a valid date. \"" + YYYYformat + "\" is a supported format.").addConstraintViolation(); + context.buildConstraintViolationWithTemplate(dsfType.getDisplayName() + " " + BundleUtil.getStringFromBundle("dataset.metadata.invalidDate") ).addConstraintViolation(); } catch (NullPointerException npe) { } @@ -143,7 +144,7 @@ public boolean isValid(DatasetFieldValue value, ConstraintValidatorContext conte } catch (Exception e) { logger.fine("Float value failed validation: " + value.getValue() + " (" + dsfType.getDisplayName() + ")"); try { - context.buildConstraintViolationWithTemplate(dsfType.getDisplayName() + " is not a valid number.").addConstraintViolation(); + context.buildConstraintViolationWithTemplate(dsfType.getDisplayName() + " " + BundleUtil.getStringFromBundle("dataset.metadata.invalidNumber") ).addConstraintViolation(); } catch (NullPointerException npe) { } @@ -157,7 +158,7 @@ public boolean isValid(DatasetFieldValue value, ConstraintValidatorContext conte Integer.parseInt(value.getValue()); } catch (Exception e) { try { - context.buildConstraintViolationWithTemplate(dsfType.getDisplayName() + " is not a valid integer.").addConstraintViolation(); + context.buildConstraintViolationWithTemplate(dsfType.getDisplayName() + " " + BundleUtil.getStringFromBundle("dataset.metadata.invalidInteger") ).addConstraintViolation(); } catch (NullPointerException npe) { } @@ -170,7 +171,7 @@ public boolean isValid(DatasetFieldValue value, ConstraintValidatorContext conte if (fieldType.equals(FieldType.URL) && !lengthOnly) { boolean isValidUrl = URLValidator.isURLValid(value.getValue()); if (!isValidUrl) { - context.buildConstraintViolationWithTemplate(dsfType.getDisplayName() + " " + value.getValue() + " {url.invalid}").addConstraintViolation(); + context.buildConstraintViolationWithTemplate(dsfType.getDisplayName() + " " + value.getValue() + " " + BundleUtil.getStringFromBundle("dataset.metadata.invalidURL")).addConstraintViolation(); return false; } } @@ -178,7 +179,7 @@ public boolean isValid(DatasetFieldValue value, ConstraintValidatorContext conte if (fieldType.equals(FieldType.EMAIL) && !lengthOnly) { boolean isValidMail = EMailValidator.isEmailValid(value.getValue()); if (!isValidMail) { - context.buildConstraintViolationWithTemplate(dsfType.getDisplayName() + " " + value.getValue() + " {email.invalid}").addConstraintViolation(); + context.buildConstraintViolationWithTemplate(dsfType.getDisplayName() + " " + value.getValue() + " " + BundleUtil.getStringFromBundle("dataset.metadata.invalidEmail")).addConstraintViolation(); return false; } } diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index 429a0d7a4e4..393c6cfad16 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -33,13 +33,14 @@ import edu.harvard.iq.dataverse.engine.command.impl.PublishDatasetCommand; import edu.harvard.iq.dataverse.engine.command.impl.PublishDataverseCommand; import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand; -import edu.harvard.iq.dataverse.export.ExportException; import edu.harvard.iq.dataverse.export.ExportService; -import edu.harvard.iq.dataverse.export.spi.Exporter; +import io.gdcc.spi.export.ExportException; +import io.gdcc.spi.export.Exporter; import edu.harvard.iq.dataverse.ingest.IngestRequest; import edu.harvard.iq.dataverse.ingest.IngestServiceBean; import edu.harvard.iq.dataverse.license.LicenseServiceBean; import edu.harvard.iq.dataverse.metadataimport.ForeignMetadataImportServiceBean; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; import edu.harvard.iq.dataverse.privateurl.PrivateUrl; import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean; import edu.harvard.iq.dataverse.privateurl.PrivateUrlUtil; @@ -48,6 +49,7 @@ import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.ArchiverUtil; import edu.harvard.iq.dataverse.util.BundleUtil; +import edu.harvard.iq.dataverse.util.DataFileComparator; import edu.harvard.iq.dataverse.util.FileSortFieldAndOrder; import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.JsfHelper; @@ -80,6 +82,8 @@ import java.util.Set; import java.util.Collection; import java.util.logging.Logger; +import java.util.stream.Collectors; + import javax.ejb.EJB; import javax.ejb.EJBException; import javax.faces.application.FacesMessage; @@ -143,6 +147,8 @@ import edu.harvard.iq.dataverse.search.SearchServiceBean; import edu.harvard.iq.dataverse.search.SearchUtil; import edu.harvard.iq.dataverse.search.SolrClientService; +import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.util.SignpostingResources; import edu.harvard.iq.dataverse.util.FileMetadataUtil; import java.util.Comparator; import org.apache.solr.client.solrj.SolrQuery; @@ -230,6 +236,8 @@ public enum DisplayMode { ExternalToolServiceBean externalToolService; @EJB SolrClientService solrClientService; + @EJB + DvObjectServiceBean dvObjectService; @Inject DataverseRequestServiceBean dvRequestService; @Inject @@ -338,7 +346,7 @@ public void setSelectedHostDataverse(Dataverse selectedHostDataverse) { private Boolean hasRsyncScript = false; - private Boolean hasTabular = false; + /*private Boolean hasTabular = false;*/ /** @@ -347,6 +355,12 @@ public void setSelectedHostDataverse(Dataverse selectedHostDataverse) { * sometimes you want to know about the current version ("no tabular files * currently"). Like all files, tabular files can be deleted. */ + /** + * There doesn't seem to be an actual real life case where we need to know + * if this dataset "has ever had a tabular file" - for all practical purposes + * only the versionHasTabular appears to be in use. I'm going to remove the + * other boolean. + */ private boolean versionHasTabular = false; private boolean showIngestSuccess; @@ -375,6 +389,8 @@ public void setShowIngestSuccess(boolean showIngestSuccess) { Map> previewToolsByFileId = new HashMap<>(); // TODO: Consider renaming "previewTools" to "filePreviewTools". List previewTools = new ArrayList<>(); + Map> fileQueryToolsByFileId = new HashMap<>(); + List fileQueryTools = new ArrayList<>(); private List datasetExploreTools; public Boolean isHasRsyncScript() { @@ -503,6 +519,16 @@ public void setRemoveUnusedTags(boolean removeUnusedTags) { private String fileSortField; private String fileSortOrder; + private boolean tagPresort = true; + private boolean folderPresort = true; + // Due to what may be a bug in PrimeFaces, the call to select a new page of + // files appears to reset the two presort booleans to false. The following + // values are a flag and duplicate booleans to remember what the new values were + // so that they can be set only in real checkbox changes. Further comments where + // these are used. + boolean isPageFlip = false; + private boolean newTagPresort = true; + private boolean newFolderPresort = true; public List> getCartList() { if (session.getUser() instanceof AuthenticatedUser) { @@ -665,70 +691,46 @@ public void showAll(){ } private List selectFileMetadatasForDisplay() { - Set searchResultsIdSet = null; - - if (isIndexedVersion()) { + final Set searchResultsIdSet; + if (isIndexedVersion() && StringUtil.isEmpty(fileLabelSearchTerm) && StringUtil.isEmpty(fileTypeFacet) && StringUtil.isEmpty(fileAccessFacet) && StringUtil.isEmpty(fileTagsFacet)) { + // Indexed version: we need facets, they are set as a side effect of getFileIdsInVersionFromSolr method. + // But, no search terms were specified, we will return the full + // list of the files in the version: we discard the result from getFileIdsInVersionFromSolr. + getFileIdsInVersionFromSolr(workingVersion.getId(), this.fileLabelSearchTerm); + // Since the search results should include the full set of fmds if all the + // terms/facets are empty, setting them to null should just be + // an optimization to skip the loop below + searchResultsIdSet = null; + } else if (isIndexedVersion()) { // We run the search even if no search term and/or facets are // specified - to generate the facet labels list: searchResultsIdSet = getFileIdsInVersionFromSolr(workingVersion.getId(), this.fileLabelSearchTerm); - // But, if no search terms were specified, we can immediately return the full - // list of the files in the version: - if (StringUtil.isEmpty(fileLabelSearchTerm) - && StringUtil.isEmpty(fileTypeFacet) - && StringUtil.isEmpty(fileAccessFacet) - && StringUtil.isEmpty(fileTagsFacet)) { - if ((StringUtil.isEmpty(fileSortField) || fileSortField.equals("name")) && StringUtil.isEmpty(fileSortOrder)) { - return workingVersion.getFileMetadatasSorted(); - } else { - searchResultsIdSet = null; - } - } - - } else { + } else if (!StringUtil.isEmpty(this.fileLabelSearchTerm)) { // No, this is not an indexed version. // If the search term was specified, we'll run a search in the db; // if not - return the full list of files in the version. // (no facets without solr!) - if (StringUtil.isEmpty(this.fileLabelSearchTerm)) { - if ((StringUtil.isEmpty(fileSortField) || fileSortField.equals("name")) && StringUtil.isEmpty(fileSortOrder)) { - return workingVersion.getFileMetadatasSorted(); - } - } else { - searchResultsIdSet = getFileIdsInVersionFromDb(workingVersion.getId(), this.fileLabelSearchTerm); - } - } - - List retList = new ArrayList<>(); - - for (FileMetadata fileMetadata : workingVersion.getFileMetadatasSorted()) { - if (searchResultsIdSet == null || searchResultsIdSet.contains(fileMetadata.getDataFile().getId())) { - retList.add(fileMetadata); - } + searchResultsIdSet = getFileIdsInVersionFromDb(workingVersion.getId(), this.fileLabelSearchTerm); + } else { + searchResultsIdSet = null; } - if ((StringUtil.isEmpty(fileSortOrder) && !("name".equals(fileSortField))) - || ("desc".equals(fileSortOrder) || !("name".equals(fileSortField)))) { - sortFileMetadatas(retList); - + final List md = workingVersion.getFileMetadatas(); + final List retList; + if (searchResultsIdSet == null) { + retList = new ArrayList<>(md); + } else { + retList = md.stream().filter(x -> searchResultsIdSet.contains(x.getDataFile().getId())).collect(Collectors.toList()); } - + sortFileMetadatas(retList); return retList; } - private void sortFileMetadatas(List fileList) { - if ("name".equals(fileSortField) && "desc".equals(fileSortOrder)) { - Collections.sort(fileList, compareByLabelZtoA); - } else if ("date".equals(fileSortField)) { - if ("desc".equals(fileSortOrder)) { - Collections.sort(fileList, compareByOldest); - } else { - Collections.sort(fileList, compareByNewest); - } - } else if ("type".equals(fileSortField)) { - Collections.sort(fileList, compareByType); - } else if ("size".equals(fileSortField)) { - Collections.sort(fileList, compareBySize); - } + private void sortFileMetadatas(final List fileList) { + + final DataFileComparator dfc = new DataFileComparator(); + final Comparator comp = dfc.compareBy(folderPresort, tagPresort, fileSortField, !"desc".equals(fileSortOrder)); + Collections.sort(fileList, comp); } private Boolean isIndexedVersion = null; @@ -1851,6 +1853,17 @@ public boolean webloaderUploadSupported() { return settingsWrapper.isWebloaderUpload() && StorageIO.isDirectUploadEnabled(dataset.getEffectiveStorageDriverId()); } + private void setIdByPersistentId() { + GlobalId gid = PidUtil.parseAsGlobalID(persistentId); + Long id = dvObjectService.findIdByGlobalId(gid, DvObject.DType.Dataset); + if (id == null) { + id = dvObjectService.findIdByAltGlobalId(gid, DvObject.DType.Dataset); + } + if (id != null) { + this.setId(id); + } + } + private String init(boolean initFull) { //System.out.println("_YE_OLDE_QUERY_COUNTER_"); // for debug purposes @@ -1861,7 +1874,12 @@ private String init(boolean initFull) { String nonNullDefaultIfKeyNotFound = ""; protocol = settingsWrapper.getValueForKey(SettingsServiceBean.Key.Protocol, nonNullDefaultIfKeyNotFound); authority = settingsWrapper.getValueForKey(SettingsServiceBean.Key.Authority, nonNullDefaultIfKeyNotFound); - if (this.getId() != null || versionId != null || persistentId != null) { // view mode for a dataset + String sortOrder = getSortOrder(); + if(sortOrder != null) { + FileMetadata.setCategorySortOrder(sortOrder); + } + + if (dataset.getId() != null || versionId != null || persistentId != null) { // view mode for a dataset DatasetVersionServiceBean.RetrieveDatasetVersionResponse retrieveDatasetVersionResponse = null; @@ -1869,44 +1887,60 @@ private String init(boolean initFull) { // Set the workingVersion and Dataset // --------------------------------------- if (persistentId != null) { - logger.fine("initializing DatasetPage with persistent ID " + persistentId); - // Set Working Version and Dataset by PersistentID - dataset = datasetService.findByGlobalId(persistentId); - if (dataset == null) { - logger.warning("No such dataset: "+persistentId); - return permissionsWrapper.notFound(); - } - logger.fine("retrieved dataset, id="+dataset.getId()); - - retrieveDatasetVersionResponse = datasetVersionService.selectRequestedVersion(dataset.getVersions(), version); - //retrieveDatasetVersionResponse = datasetVersionService.retrieveDatasetVersionByPersistentId(persistentId, version); - this.workingVersion = retrieveDatasetVersionResponse.getDatasetVersion(); - logger.fine("retrieved version: id: " + workingVersion.getId() + ", state: " + this.workingVersion.getVersionState()); - - } else if (this.getId() != null) { + setIdByPersistentId(); + } + + if (this.getId() != null) { // Set Working Version and Dataset by Datasaet Id and Version + + // We are only performing these lookups to obtain the database id + // of the version that we are displaying, and then we will use it + // to perform a .findDeep(versionId); see below. + + // TODO: replace the code block below, the combination of + // datasetService.find(id) and datasetVersionService.selectRequestedVersion() + // with some optimized, direct query-based way of obtaining + // the numeric id of the requested DatasetVersion (and that's + // all we need, we are not using any of the entities produced + // below. + dataset = datasetService.find(this.getId()); + if (dataset == null) { logger.warning("No such dataset: "+dataset); return permissionsWrapper.notFound(); } //retrieveDatasetVersionResponse = datasetVersionService.retrieveDatasetVersionById(dataset.getId(), version); retrieveDatasetVersionResponse = datasetVersionService.selectRequestedVersion(dataset.getVersions(), version); + if (retrieveDatasetVersionResponse == null) { + return permissionsWrapper.notFound(); + } this.workingVersion = retrieveDatasetVersionResponse.getDatasetVersion(); - logger.info("retreived version: id: " + workingVersion.getId() + ", state: " + this.workingVersion.getVersionState()); - - } else if (versionId != null) { - // TODO: 4.2.1 - this method is broken as of now! - // Set Working Version and Dataset by DatasaetVersion Id - //retrieveDatasetVersionResponse = datasetVersionService.retrieveDatasetVersionByVersionId(versionId); - + logger.fine("retrieved version: id: " + workingVersion.getId() + ", state: " + this.workingVersion.getVersionState()); + + versionId = workingVersion.getId(); + + this.workingVersion = null; + this.dataset = null; + + } + + // ... And now the "real" working version lookup: + + if (versionId != null) { + this.workingVersion = datasetVersionService.findDeep(versionId); + dataset = workingVersion.getDataset(); + } + + if (workingVersion == null) { + logger.warning("Failed to retrieve version"); + return permissionsWrapper.notFound(); } + this.maxFileUploadSizeInBytes = systemConfig.getMaxFileUploadSizeForStore(dataset.getEffectiveStorageDriverId()); - if (retrieveDatasetVersionResponse == null) { - return permissionsWrapper.notFound(); - } + switch (selectTab){ case "dataFilesTab": @@ -1923,16 +1957,6 @@ private String init(boolean initFull) { break; } - //this.dataset = this.workingVersion.getDataset(); - - // end: Set the workingVersion and Dataset - // --------------------------------------- - // Is the DatasetVersion or Dataset null? - // - if (workingVersion == null || this.dataset == null) { - return permissionsWrapper.notFound(); - } - // Is the Dataset harvested? if (dataset.isHarvested()) { @@ -1960,7 +1984,7 @@ private String init(boolean initFull) { return permissionsWrapper.notAuthorized(); } - if (!retrieveDatasetVersionResponse.wasRequestedVersionRetrieved()) { + if (retrieveDatasetVersionResponse != null && !retrieveDatasetVersionResponse.wasRequestedVersionRetrieved()) { //msg("checkit " + retrieveDatasetVersionResponse.getDifferentVersionMessage()); JsfHelper.addWarningMessage(retrieveDatasetVersionResponse.getDifferentVersionMessage());//BundleUtil.getStringFromBundle("dataset.message.metadataSuccess")); } @@ -1981,11 +2005,6 @@ private String init(boolean initFull) { // init the list of FileMetadatas if (workingVersion.isDraft() && canUpdateDataset()) { readOnly = false; - } else { - // an attempt to retreive both the filemetadatas and datafiles early on, so that - // we don't have to do so later (possibly, many more times than necessary): - AuthenticatedUser au = session.getUser() instanceof AuthenticatedUser ? (AuthenticatedUser) session.getUser() : null; - datafileService.findFileMetadataOptimizedExperimental(dataset, workingVersion, au); } // This will default to all the files in the version, if the search term // parameter hasn't been specified yet: @@ -2049,7 +2068,7 @@ private String init(boolean initFull) { if ( isEmpty(dataset.getIdentifier()) && systemConfig.directUploadEnabled(dataset) ) { CommandContext ctxt = commandEngine.getContext(); GlobalIdServiceBean idServiceBean = GlobalIdServiceBean.getBean(ctxt); - dataset.setIdentifier(ctxt.datasets().generateDatasetIdentifier(dataset, idServiceBean)); + dataset.setIdentifier(idServiceBean.generateDatasetIdentifier(dataset)); } dataverseTemplates.addAll(dataverseService.find(ownerId).getTemplates()); if (!dataverseService.find(ownerId).isTemplateRoot()) { @@ -2112,23 +2131,18 @@ private String init(boolean initFull) { displayLockInfo(dataset); displayPublishMessage(); + // TODO: replace this loop, and the loop in the method that calculates + // the total "originals" size of the dataset with direct custom queries; + // then we'll be able to drop the lookup hint for DataTable from the + // findDeep() method for the version and further speed up the lookup + // a little bit. for (FileMetadata fmd : workingVersion.getFileMetadatas()) { if (fmd.getDataFile().isTabularData()) { versionHasTabular = true; break; } } - for(DataFile f : dataset.getFiles()) { - // TODO: Consider uncommenting this optimization. -// if (versionHasTabular) { -// hasTabular = true; -// break; -// } - if(f.isTabularData()) { - hasTabular = true; - break; - } - } + //Show ingest success message if refresh forces a page reload after ingest success //This is needed to display the explore buttons (the fileDownloadHelper needs to be reloaded via page if (showIngestSuccess) { @@ -2138,6 +2152,7 @@ private String init(boolean initFull) { configureTools = externalToolService.findFileToolsByType(ExternalTool.Type.CONFIGURE); exploreTools = externalToolService.findFileToolsByType(ExternalTool.Type.EXPLORE); previewTools = externalToolService.findFileToolsByType(ExternalTool.Type.PREVIEW); + fileQueryTools = externalToolService.findFileToolsByType(ExternalTool.Type.QUERY); datasetExploreTools = externalToolService.findDatasetToolsByType(ExternalTool.Type.EXPLORE); rowsPerPage = 10; if (dataset.getId() != null && canUpdateDataset()) { @@ -2171,10 +2186,29 @@ private void displayPublishMessage(){ if (workingVersion.isDraft() && workingVersion.getId() != null && canUpdateDataset() && !dataset.isLockedFor(DatasetLock.Reason.finalizePublication) && (canPublishDataset() || !dataset.isLockedFor(DatasetLock.Reason.InReview) )){ - JsfHelper.addWarningMessage(datasetService.getReminderString(dataset, canPublishDataset())); + JsfHelper.addWarningMessage(datasetService.getReminderString(dataset, canPublishDataset(), false, isValid())); } } + Boolean valid = null; + + public boolean isValid() { + if (valid == null) { + DatasetVersion version = dataset.getLatestVersion(); + if (!version.isDraft()) { + valid = true; + } + DatasetVersion newVersion = version.cloneDatasetVersion(); + newVersion.setDatasetFields(newVersion.initDatasetFields()); + valid = newVersion.isValid(); + } + return valid; + } + + public boolean isValidOrCanReviewIncomplete() { + return isValid() || JvmSettings.UI_ALLOW_REVIEW_INCOMPLETE.lookupOptional(Boolean.class).orElse(false); + } + private void displayLockInfo(Dataset dataset) { // Various info messages, when the dataset is locked (for various reasons): if (dataset.isLocked() && canUpdateDataset()) { @@ -2243,6 +2277,19 @@ private void displayLockInfo(Dataset dataset) { } + public String getSortOrder() { + return settingsWrapper.getValueForKey(SettingsServiceBean.Key.CategoryOrder, null); + } + + public boolean orderByFolder() { + return settingsWrapper.isTrueForKey(SettingsServiceBean.Key.OrderByFolder, true); + } + + public boolean allowUserManagementOfOrder() { + return settingsWrapper.isTrueForKey(SettingsServiceBean.Key.AllowUserManagementOfOrder, false); + } + + private Boolean fileTreeViewRequired = null; public boolean isFileTreeViewRequired() { @@ -2265,6 +2312,7 @@ public String getFileDisplayMode() { } public void setFileDisplayMode(String fileDisplayMode) { + isPageFlip = true; if ("Table".equals(fileDisplayMode)) { this.fileDisplayMode = FileDisplayStyle.TABLE; } else { @@ -2276,13 +2324,6 @@ public boolean isFileDisplayTable() { return fileDisplayMode == FileDisplayStyle.TABLE; } - public void toggleFileDisplayMode() { - if (fileDisplayMode == FileDisplayStyle.TABLE) { - fileDisplayMode = FileDisplayStyle.TREE; - } else { - fileDisplayMode = FileDisplayStyle.TABLE; - } - } public boolean isFileDisplayTree() { return fileDisplayMode == FileDisplayStyle.TREE; } @@ -2386,9 +2427,9 @@ private DefaultTreeNode createFileTreeNode(FileMetadata fileMetadata, TreeNode p return fileNode; } - public boolean isHasTabular() { + /*public boolean isHasTabular() { return hasTabular; - } + }*/ public boolean isVersionHasTabular() { return versionHasTabular; @@ -2802,54 +2843,52 @@ public void refresh(ActionEvent e) { refresh(); } + + public void sort() { + // This is called as the presort checkboxes' listener when the user is actually + // clicking in the checkbox. It does appear to happen after the setTagPresort + // and setFolderPresort calls. + // So -we know this isn't a pageflip and at this point can update to use the new + // values. + isPageFlip = false; + if (!newTagPresort == tagPresort) { + tagPresort = newTagPresort; + } + if (!newFolderPresort == folderPresort) { + folderPresort = newFolderPresort; + } + sortFileMetadatas(fileMetadatasSearch); + JsfHelper.addSuccessMessage(BundleUtil.getStringFromBundle("file.results.presort.change.success")); + } + public String refresh() { logger.fine("refreshing"); //dataset = datasetService.find(dataset.getId()); dataset = null; + workingVersion = null; logger.fine("refreshing working version"); DatasetVersionServiceBean.RetrieveDatasetVersionResponse retrieveDatasetVersionResponse = null; - if (persistentId != null) { - //retrieveDatasetVersionResponse = datasetVersionService.retrieveDatasetVersionByPersistentId(persistentId, version); - dataset = datasetService.findByGlobalId(persistentId); - retrieveDatasetVersionResponse = datasetVersionService.selectRequestedVersion(dataset.getVersions(), version); - } else if (versionId != null) { - retrieveDatasetVersionResponse = datasetVersionService.retrieveDatasetVersionByVersionId(versionId); - } else if (dataset.getId() != null) { - //retrieveDatasetVersionResponse = datasetVersionService.retrieveDatasetVersionById(dataset.getId(), version); - dataset = datasetService.find(dataset.getId()); - retrieveDatasetVersionResponse = datasetVersionService.selectRequestedVersion(dataset.getVersions(), version); - } + if (versionId != null) { + // versionId must have been set by now, in the init() method, + // regardless of how the page was originally called - by the dataset + // database id, by the persistent identifier, or by the db id of + // the version. + this.workingVersion = datasetVersionService.findDeep(versionId); + dataset = workingVersion.getDataset(); + } + - if (retrieveDatasetVersionResponse == null) { + if (this.workingVersion == null) { // TODO: // should probably redirect to the 404 page, if we can't find // this version anymore. // -- L.A. 4.2.3 return ""; } - this.workingVersion = retrieveDatasetVersionResponse.getDatasetVersion(); - - if (this.workingVersion == null) { - // TODO: - // same as the above - - return ""; - } - - if (dataset == null) { - // this would be the case if we were retrieving the version by - // the versionId, above. - this.dataset = this.workingVersion.getDataset(); - } - - if (readOnly) { - AuthenticatedUser au = session.getUser() instanceof AuthenticatedUser ? (AuthenticatedUser) session.getUser() : null; - datafileService.findFileMetadataOptimizedExperimental(dataset, workingVersion, au); - } fileMetadatasSearch = selectFileMetadatasForDisplay(); @@ -2862,9 +2901,9 @@ public String refresh() { //SEK 12/20/2019 - since we are ingesting a file we know that there is a current draft version lockedDueToIngestVar = null; if (canViewUnpublishedDataset()) { - return "/dataset.xhtml?persistentId=" + dataset.getGlobalIdString() + "&showIngestSuccess=true&version=DRAFT&faces-redirect=true"; + return "/dataset.xhtml?persistentId=" + dataset.getGlobalId().asString() + "&showIngestSuccess=true&version=DRAFT&faces-redirect=true"; } else { - return "/dataset.xhtml?persistentId=" + dataset.getGlobalIdString() + "&showIngestSuccess=true&faces-redirect=true"; + return "/dataset.xhtml?persistentId=" + dataset.getGlobalId().asString() + "&showIngestSuccess=true&faces-redirect=true"; } } @@ -3028,19 +3067,32 @@ public void setTooLargeToDownload(boolean tooLargeToDownload) { this.tooLargeToDownload = tooLargeToDownload; } + private Long sizeOfDatasetArchival = null; + private Long sizeOfDatasetOriginal = null; + + public Long getSizeOfDatasetNumeric() { - if (this.hasTabular){ + if (this.versionHasTabular){ return Math.min(getSizeOfDatasetOrigNumeric(), getSizeOfDatasetArchivalNumeric()); } return getSizeOfDatasetOrigNumeric(); } public Long getSizeOfDatasetOrigNumeric() { - return DatasetUtil.getDownloadSizeNumeric(workingVersion, true); + if (versionHasTabular) { + if (sizeOfDatasetOriginal == null) { + sizeOfDatasetOriginal = DatasetUtil.getDownloadSizeNumeric(workingVersion, true); + } + return sizeOfDatasetOriginal; + } + return getSizeOfDatasetArchivalNumeric(); } public Long getSizeOfDatasetArchivalNumeric() { - return DatasetUtil.getDownloadSizeNumeric(workingVersion, false); + if (sizeOfDatasetArchival == null) { + sizeOfDatasetArchival = DatasetUtil.getDownloadSizeNumeric(workingVersion, false); + } + return sizeOfDatasetArchival; } public String getSizeOfSelectedAsString(){ @@ -3595,7 +3647,7 @@ public String save() { //ToDo - could drop use of selectedTemplate and just use the persistent dataset.getTemplate() if ( selectedTemplate != null ) { if ( isSessionUserAuthenticated() ) { - cmd = new CreateNewDatasetCommand(dataset, dvRequestService.getDataverseRequest(), false, selectedTemplate); + cmd = new CreateNewDatasetCommand(dataset, dvRequestService.getDataverseRequest(), selectedTemplate); } else { JH.addMessage(FacesMessage.SEVERITY_FATAL, BundleUtil.getStringFromBundle("dataset.create.authenticatedUsersOnly")); return null; @@ -3622,9 +3674,9 @@ public String save() { ((UpdateDatasetVersionCommand) cmd).setValidateLenient(true); } dataset = commandEngine.submit(cmd); - for (DatasetField df : dataset.getLatestVersion().getDatasetFields()) { + for (DatasetField df : dataset.getLatestVersion().getFlatDatasetFields()) { logger.fine("Found id: " + df.getDatasetFieldType().getId()); - if (fieldService.getCVocConf(false).containsKey(df.getDatasetFieldType().getId())) { + if (fieldService.getCVocConf(true).containsKey(df.getDatasetFieldType().getId())) { fieldService.registerExternalVocabValues(df); } } @@ -3791,7 +3843,7 @@ private String returnToLatestVersion(){ setReleasedVersionTabList(resetReleasedVersionTabList()); newFiles.clear(); editMode = null; - return "/dataset.xhtml?persistentId=" + dataset.getGlobalIdString() + "&version="+ workingVersion.getFriendlyVersionNumber() + "&faces-redirect=true"; + return "/dataset.xhtml?persistentId=" + dataset.getGlobalId().asString() + "&version="+ workingVersion.getFriendlyVersionNumber() + "&faces-redirect=true"; } private String returnToDatasetOnly(){ @@ -3801,7 +3853,7 @@ private String returnToDatasetOnly(){ } private String returnToDraftVersion(){ - return "/dataset.xhtml?persistentId=" + dataset.getGlobalIdString() + "&version=DRAFT" + "&faces-redirect=true"; + return "/dataset.xhtml?persistentId=" + dataset.getGlobalId().asString() + "&version=DRAFT" + "&faces-redirect=true"; } public String cancel() { @@ -4414,6 +4466,8 @@ public List< String[]> getExporters(){ try { exporter = ExportService.getInstance().getExporter(formatName); } catch (ExportException ex) { + logger.warning("Failed to get : " + formatName); + logger.warning(ex.getLocalizedMessage()); exporter = null; } if (exporter != null && exporter.isAvailableToUsers()) { @@ -4422,7 +4476,7 @@ public List< String[]> getExporters(){ String[] temp = new String[2]; temp[0] = formatDisplayName; - temp[1] = myHostURL + "/api/datasets/export?exporter=" + formatName + "&persistentId=" + dataset.getGlobalIdString(); + temp[1] = myHostURL + "/api/datasets/export?exporter=" + formatName + "&persistentId=" + dataset.getGlobalId().asString(); retList.add(temp); } } @@ -5042,10 +5096,9 @@ public boolean isFileAccessRequestMultiButtonRequired(){ // return false; } for (FileMetadata fmd : workingVersion.getFileMetadatas()){ + AuthenticatedUser authenticatedUser = (AuthenticatedUser) session.getUser(); //Change here so that if all restricted files have pending requests there's no Request Button - if ((!this.fileDownloadHelper.canDownloadFile(fmd) && (fmd.getDataFile().getFileAccessRequesters() == null - || ( fmd.getDataFile().getFileAccessRequesters() != null - && !fmd.getDataFile().getFileAccessRequesters().contains((AuthenticatedUser)session.getUser()))))){ + if ((!this.fileDownloadHelper.canDownloadFile(fmd) && !fmd.getDataFile().containsFileAccessRequestFromUser(authenticatedUser))) { return true; } } @@ -5456,12 +5509,25 @@ public boolean isShowPreviewButton(Long fileId) { List previewTools = getPreviewToolsForDataFile(fileId); return previewTools.size() > 0; } + + public boolean isShowQueryButton(Long fileId) { + DataFile dataFile = datafileService.find(fileId); + + if(dataFile.isRestricted() || !dataFile.isReleased() || FileUtil.isActivelyEmbargoed(dataFile)){ + return false; + } + + List fileQueryTools = getQueryToolsForDataFile(fileId); + return fileQueryTools.size() > 0; + } public List getPreviewToolsForDataFile(Long fileId) { return getCachedToolsForDataFile(fileId, ExternalTool.Type.PREVIEW); } - + public List getQueryToolsForDataFile(Long fileId) { + return getCachedToolsForDataFile(fileId, ExternalTool.Type.QUERY); + } public List getConfigureToolsForDataFile(Long fileId) { return getCachedToolsForDataFile(fileId, ExternalTool.Type.CONFIGURE); } @@ -5486,6 +5552,10 @@ public List getCachedToolsForDataFile(Long fileId, ExternalTool.Ty cachedToolsByFileId = previewToolsByFileId; externalTools = previewTools; break; + case QUERY: + cachedToolsByFileId = fileQueryToolsByFileId; + externalTools = fileQueryTools; + break; default: break; } @@ -5556,6 +5626,10 @@ public void clearSelection() { } public void fileListingPaginatorListener(PageEvent event) { + // Changing to a new page of files - set this so we can ignore changes to the + // presort checkboxes. (This gets called before the set calls for the presorts + // get called.) + isPageFlip=true; setFilePaginatorPage(event.getPage()); } @@ -5672,52 +5746,34 @@ public boolean isSomeVersionArchived() { return someVersionArchived; } - private static Date getFileDateToCompare(FileMetadata fileMetadata) { - DataFile datafile = fileMetadata.getDataFile(); - - if (datafile.isReleased()) { - return datafile.getPublicationDate(); + public boolean isTagPresort() { + return this.tagPresort; } - return datafile.getCreateDate(); - } - - private static final Comparator compareByLabelZtoA = new Comparator() { - @Override - public int compare(FileMetadata o1, FileMetadata o2) { - return o2.getLabel().toUpperCase().compareTo(o1.getLabel().toUpperCase()); + public void setTagPresort(boolean tagPresort) { + // Record the new value + newTagPresort = tagPresort && (null != getSortOrder()); + // If this is not a page flip, it should be a real change to the presort + // boolean that we should use. + if (!isPageFlip) { + this.tagPresort = tagPresort && (null != getSortOrder()); + } } - }; - private static final Comparator compareByNewest = new Comparator() { - @Override - public int compare(FileMetadata o1, FileMetadata o2) { - return getFileDateToCompare(o2).compareTo(getFileDateToCompare(o1)); + public boolean isFolderPresort() { + return this.folderPresort; } - }; - private static final Comparator compareByOldest = new Comparator() { - @Override - public int compare(FileMetadata o1, FileMetadata o2) { - return getFileDateToCompare(o1).compareTo(getFileDateToCompare(o2)); - } - }; - - private static final Comparator compareBySize = new Comparator() { - @Override - public int compare(FileMetadata o1, FileMetadata o2) { - return (new Long(o1.getDataFile().getFilesize())).compareTo(new Long(o2.getDataFile().getFilesize())); + public void setFolderPresort(boolean folderPresort) { + //Record the new value + newFolderPresort = folderPresort && orderByFolder(); + // If this is not a page flip, it should be a real change to the presort + // boolean that we should use. + if (!isPageFlip) { + this.folderPresort = folderPresort && orderByFolder(); + } } - }; - private static final Comparator compareByType = new Comparator() { - @Override - public int compare(FileMetadata o1, FileMetadata o2) { - String type1 = StringUtil.isEmpty(o1.getDataFile().getFriendlyType()) ? "" : o1.getDataFile().getContentType(); - String type2 = StringUtil.isEmpty(o2.getDataFile().getFriendlyType()) ? "" : o2.getDataFile().getContentType(); - return type1.compareTo(type2); - } - }; public void explore(ExternalTool externalTool) { ApiToken apiToken = null; @@ -5797,7 +5853,7 @@ public Set> getMetadataLanguages() { } public List getVocabScripts() { - return fieldService.getVocabScripts(settingsWrapper.getCVocConf()); + return fieldService.getVocabScripts(settingsWrapper.getCVocConf(false)); } public String getFieldLanguage(String languages) { @@ -6046,8 +6102,7 @@ public boolean downloadingRestrictedFiles() { } return false; } - - + //Determines whether this Dataset uses a public store and therefore doesn't support embargoed or restricted files public boolean isHasPublicStore() { return settingsWrapper.isTrueForKey(SettingsServiceBean.Key.PublicInstall, StorageIO.isPublicStore(dataset.getEffectiveStorageDriverId())); @@ -6080,5 +6135,26 @@ public String getWebloaderUrlForDataset(Dataset d) { return null; } } + + /** + * Add Signposting + * + * @return String + */ + + String signpostingLinkHeader = null; + + public String getSignpostingLinkHeader() { + if (!workingVersion.isReleased()) { + return null; + } + if (signpostingLinkHeader == null) { + SignpostingResources sr = new SignpostingResources(systemConfig, workingVersion, + JvmSettings.SIGNPOSTING_LEVEL1_AUTHOR_LIMIT.lookupOptional().orElse(""), + JvmSettings.SIGNPOSTING_LEVEL1_ITEM_LIMIT.lookupOptional().orElse("")); + signpostingLinkHeader = sr.getLinks(); + } + return signpostingLinkHeader; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index 91ec050fe5c..c93236f347b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -1,5 +1,6 @@ package edu.harvard.iq.dataverse; +import edu.harvard.iq.dataverse.DatasetVersion.VersionState; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; @@ -38,6 +39,7 @@ import javax.ejb.TransactionAttributeType; import javax.inject.Named; import javax.persistence.EntityManager; +import javax.persistence.LockModeType; import javax.persistence.NoResultException; import javax.persistence.PersistenceContext; import javax.persistence.Query; @@ -105,6 +107,38 @@ public Dataset find(Object pk) { return em.find(Dataset.class, pk); } + /** + * Retrieve a dataset with the deep underlying structure in one query execution. + * This is a more optimal choice when accessing files of a dataset. + * In a contrast, the find() method does not pre-fetch the file objects and results in point queries when accessing these objects. + * Since the files have a deep structure, many queries can be prevented by using the findDeep() method, especially for large datasets + * containing many files, and when iterating over all the files. + * When you are not going to access the file objects, the default find() method is better because of the lazy loading. + * @return a dataset with pre-fetched file objects + */ + public Dataset findDeep(Object pk) { + return (Dataset) em.createNamedQuery("Dataset.findById") + .setParameter("id", pk) + // Optimization hints: retrieve all data in one query; this prevents point queries when iterating over the files + .setHint("eclipselink.left-join-fetch", "o.files.ingestRequest") + .setHint("eclipselink.left-join-fetch", "o.files.thumbnailForDataset") + .setHint("eclipselink.left-join-fetch", "o.files.dataTables") + .setHint("eclipselink.left-join-fetch", "o.files.auxiliaryFiles") + .setHint("eclipselink.left-join-fetch", "o.files.ingestReports") + .setHint("eclipselink.left-join-fetch", "o.files.dataFileTags") + .setHint("eclipselink.left-join-fetch", "o.files.fileMetadatas") + .setHint("eclipselink.left-join-fetch", "o.files.fileMetadatas.fileCategories") + //.setHint("eclipselink.left-join-fetch", "o.files.guestbookResponses") + .setHint("eclipselink.left-join-fetch", "o.files.embargo") + .setHint("eclipselink.left-join-fetch", "o.files.fileAccessRequests") + .setHint("eclipselink.left-join-fetch", "o.files.owner") + .setHint("eclipselink.left-join-fetch", "o.files.releaseUser") + .setHint("eclipselink.left-join-fetch", "o.files.creator") + .setHint("eclipselink.left-join-fetch", "o.files.alternativePersistentIndentifiers") + .setHint("eclipselink.left-join-fetch", "o.files.roleAssignments") + .getSingleResult(); + } + public List findByOwnerId(Long ownerId) { return findByOwnerId(ownerId, false); } @@ -199,8 +233,10 @@ public List findAllUnindexed() { } //Used in datasets listcurationstatus API - public List findAllUnpublished() { - return em.createQuery("SELECT object(o) FROM Dataset o, DvObject d WHERE d.id=o.id and d.publicationDate IS null ORDER BY o.id ASC", Dataset.class).getResultList(); + public List findAllWithDraftVersion() { + TypedQuery query = em.createQuery("SELECT object(d) FROM Dataset d, DatasetVersion v WHERE d.id=v.dataset.id and v.versionState=:state ORDER BY d.id ASC", Dataset.class); + query.setParameter("state", VersionState.DRAFT); + return query.getResultList(); } /** @@ -280,12 +316,12 @@ public Dataset merge( Dataset ds ) { } public Dataset findByGlobalId(String globalId) { - Dataset retVal = (Dataset) dvObjectService.findByGlobalId(globalId, "Dataset"); + Dataset retVal = (Dataset) dvObjectService.findByGlobalId(globalId, DvObject.DType.Dataset); if (retVal != null){ return retVal; } else { //try to find with alternative PID - return (Dataset) dvObjectService.findByGlobalId(globalId, "Dataset", true); + return (Dataset) dvObjectService.findByAltGlobalId(globalId, DvObject.DType.Dataset); } } @@ -316,85 +352,11 @@ public void instantiateDatasetInNewTransaction(Long id, boolean includeVariables } } - public String generateDatasetIdentifier(Dataset dataset, GlobalIdServiceBean idServiceBean) { - String identifierType = settingsService.getValueForKey(SettingsServiceBean.Key.IdentifierGenerationStyle, "randomString"); - String shoulder = settingsService.getValueForKey(SettingsServiceBean.Key.Shoulder, ""); - - switch (identifierType) { - case "randomString": - return generateIdentifierAsRandomString(dataset, idServiceBean, shoulder); - case "storedProcGenerated": - return generateIdentifierFromStoredProcedure(dataset, idServiceBean, shoulder); - default: - /* Should we throw an exception instead?? -- L.A. 4.6.2 */ - return generateIdentifierAsRandomString(dataset, idServiceBean, shoulder); - } - } - private String generateIdentifierAsRandomString(Dataset dataset, GlobalIdServiceBean idServiceBean, String shoulder) { - String identifier = null; - do { - identifier = shoulder + RandomStringUtils.randomAlphanumeric(6).toUpperCase(); - } while (!isIdentifierLocallyUnique(identifier, dataset)); - - return identifier; - } - - private String generateIdentifierFromStoredProcedure(Dataset dataset, GlobalIdServiceBean idServiceBean, String shoulder) { - - String identifier; - do { - StoredProcedureQuery query = this.em.createNamedStoredProcedureQuery("Dataset.generateIdentifierFromStoredProcedure"); - query.execute(); - String identifierFromStoredProcedure = (String) query.getOutputParameterValue(1); - // some diagnostics here maybe - is it possible to determine that it's failing - // because the stored procedure hasn't been created in the database? - if (identifierFromStoredProcedure == null) { - return null; - } - identifier = shoulder + identifierFromStoredProcedure; - } while (!isIdentifierLocallyUnique(identifier, dataset)); - - return identifier; - } - - /** - * Check that a identifier entered by the user is unique (not currently used - * for any other study in this Dataverse Network) also check for duplicate - * in EZID if needed - * @param userIdentifier - * @param dataset - * @param persistentIdSvc - * @return {@code true} if the identifier is unique, {@code false} otherwise. - */ - public boolean isIdentifierUnique(String userIdentifier, Dataset dataset, GlobalIdServiceBean persistentIdSvc) { - if ( ! isIdentifierLocallyUnique(userIdentifier, dataset) ) return false; // duplication found in local database - - // not in local DB, look in the persistent identifier service - try { - return ! persistentIdSvc.alreadyExists(dataset); - } catch (Exception e){ - //we can live with failure - means identifier not found remotely - } - - return true; - } - - public boolean isIdentifierLocallyUnique(Dataset dataset) { - return isIdentifierLocallyUnique(dataset.getIdentifier(), dataset); - } - - public boolean isIdentifierLocallyUnique(String identifier, Dataset dataset) { - return em.createNamedQuery("Dataset.findByIdentifierAuthorityProtocol") - .setParameter("identifier", identifier) - .setParameter("authority", dataset.getAuthority()) - .setParameter("protocol", dataset.getProtocol()) - .getResultList().isEmpty(); - } public Long getMaximumExistingDatafileIdentifier(Dataset dataset) { //Cannot rely on the largest table id having the greatest identifier counter - long zeroFiles = new Long(0); + long zeroFiles = 0L; Long retVal = zeroFiles; Long testVal; List idResults; @@ -411,7 +373,7 @@ public Long getMaximumExistingDatafileIdentifier(Dataset dataset) { for (Object raw: idResults){ String identifier = (String) raw; identifier = identifier.substring(identifier.lastIndexOf("/") + 1); - testVal = new Long(identifier) ; + testVal = Long.valueOf(identifier) ; if (testVal > retVal){ retVal = testVal; } @@ -781,10 +743,10 @@ public void exportAllDatasets(boolean forceReExport) { countAll++; try { recordService.exportAllFormatsInNewTransaction(dataset); - exportLogger.info("Success exporting dataset: " + dataset.getDisplayName() + " " + dataset.getGlobalIdString()); + exportLogger.info("Success exporting dataset: " + dataset.getDisplayName() + " " + dataset.getGlobalId().asString()); countSuccess++; } catch (Exception ex) { - exportLogger.info("Error exporting dataset: " + dataset.getDisplayName() + " " + dataset.getGlobalIdString() + "; " + ex.getMessage()); + exportLogger.log(Level.INFO, "Error exporting dataset: " + dataset.getDisplayName() + " " + dataset.getGlobalId().asString() + "; " + ex.getMessage(), ex); countError++; } } @@ -801,7 +763,6 @@ public void exportAllDatasets(boolean forceReExport) { } } - @Asynchronous public void reExportDatasetAsync(Dataset dataset) { @@ -821,9 +782,9 @@ public void exportDataset(Dataset dataset, boolean forceReExport) { || dataset.getLastExportTime().before(publicationDate)))) { try { recordService.exportAllFormatsInNewTransaction(dataset); - logger.info("Success exporting dataset: " + dataset.getDisplayName() + " " + dataset.getGlobalIdString()); + logger.info("Success exporting dataset: " + dataset.getDisplayName() + " " + dataset.getGlobalId().asString()); } catch (Exception ex) { - logger.info("Error exporting dataset: " + dataset.getDisplayName() + " " + dataset.getGlobalIdString() + "; " + ex.getMessage()); + logger.log(Level.INFO, "Error exporting dataset: " + dataset.getDisplayName() + " " + dataset.getGlobalId().asString() + "; " + ex.getMessage(), ex); } } } @@ -831,13 +792,9 @@ public void exportDataset(Dataset dataset, boolean forceReExport) { } - public String getReminderString(Dataset dataset, boolean canPublishDataset) { - return getReminderString( dataset, canPublishDataset, false); - } - //get a string to add to save success message //depends on page (dataset/file) and user privleges - public String getReminderString(Dataset dataset, boolean canPublishDataset, boolean filePage) { + public String getReminderString(Dataset dataset, boolean canPublishDataset, boolean filePage, boolean isValid) { String reminderString; @@ -863,6 +820,10 @@ public String getReminderString(Dataset dataset, boolean canPublishDataset, bool } } + if (!isValid) { + reminderString = reminderString + "
" + BundleUtil.getStringFromBundle("dataset.message.incomplete.warning") + ""; + } + if (reminderString != null) { return reminderString; } else { @@ -1019,7 +980,7 @@ public void obtainPersistentIdentifiersForDatafiles(Dataset dataset) { maxIdentifier++; datafile.setIdentifier(datasetIdentifier + "/" + maxIdentifier.toString()); } else { - datafile.setIdentifier(fileService.generateDataFileIdentifier(datafile, idServiceBean)); + datafile.setIdentifier(idServiceBean.generateDataFileIdentifier(datafile)); } if (datafile.getProtocol() == null) { diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java b/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java index c21861a1bf4..9d5c27ae9fd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java @@ -3,10 +3,12 @@ import edu.harvard.iq.dataverse.util.MarkupChecker; import edu.harvard.iq.dataverse.util.PersonOrOrgUtil; import edu.harvard.iq.dataverse.util.BundleUtil; +import edu.harvard.iq.dataverse.util.DataFileComparator; import edu.harvard.iq.dataverse.DatasetFieldType.FieldType; import edu.harvard.iq.dataverse.branding.BrandingUtil; import edu.harvard.iq.dataverse.dataset.DatasetUtil; import edu.harvard.iq.dataverse.license.License; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.StringUtil; import edu.harvard.iq.dataverse.util.SystemConfig; @@ -15,9 +17,7 @@ import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; import edu.harvard.iq.dataverse.workflows.WorkflowComment; import java.io.Serializable; -import java.net.URL; import java.sql.Timestamp; -import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; @@ -55,7 +55,6 @@ import javax.validation.ConstraintViolation; import javax.validation.Validation; import javax.validation.Validator; -import javax.validation.ValidatorFactory; import javax.validation.constraints.Size; import org.apache.commons.lang3.StringUtils; @@ -67,7 +66,9 @@ @NamedQueries({ @NamedQuery(name = "DatasetVersion.findUnarchivedReleasedVersion", query = "SELECT OBJECT(o) FROM DatasetVersion AS o WHERE o.dataset.harvestedFrom IS NULL and o.releaseTime IS NOT NULL and o.archivalCopyLocation IS NULL" - )}) + ), + @NamedQuery(name = "DatasetVersion.findById", + query = "SELECT o FROM DatasetVersion o LEFT JOIN FETCH o.fileMetadatas WHERE o.id=:id")}) @Entity @@ -77,6 +78,7 @@ public class DatasetVersion implements Serializable { private static final Logger logger = Logger.getLogger(DatasetVersion.class.getCanonicalName()); + private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); /** * Convenience comparator to compare dataset versions by their version number. @@ -243,14 +245,34 @@ public List getFileMetadatas() { } public List getFileMetadatasSorted() { - Collections.sort(fileMetadatas, FileMetadata.compareByLabel); + + /* + * fileMetadatas can sometimes be an + * org.eclipse.persistence.indirection.IndirectList When that happens, the + * comparator in the Collections.sort below is not called, possibly due to + * https://bugs.eclipse.org/bugs/show_bug.cgi?id=446236 which is Java 1.8+ + * specific Converting to an ArrayList solves the problem, but the longer term + * solution may be in avoiding the IndirectList or moving to a new version of + * the jar it is in. + */ + if(!(fileMetadatas instanceof ArrayList)) { + List newFMDs = new ArrayList(); + for(FileMetadata fmd: fileMetadatas) { + newFMDs.add(fmd); + } + setFileMetadatas(newFMDs); + } + + DataFileComparator dfc = new DataFileComparator(); + Collections.sort(fileMetadatas, dfc.compareBy(true, null!=FileMetadata.getCategorySortOrder(), "name", true)); return fileMetadatas; } public List getFileMetadatasSortedByLabelAndFolder() { ArrayList fileMetadatasCopy = new ArrayList<>(); fileMetadatasCopy.addAll(fileMetadatas); - Collections.sort(fileMetadatasCopy, FileMetadata.compareByLabelAndFolder); + DataFileComparator dfc = new DataFileComparator(); + Collections.sort(fileMetadatasCopy, dfc.compareBy(true, null!=FileMetadata.getCategorySortOrder(), "name", true)); return fileMetadatasCopy; } @@ -389,7 +411,7 @@ public void setDeaccessionLink(String deaccessionLink) { } public GlobalId getDeaccessionLinkAsGlobalId() { - return new GlobalId(deaccessionLink); + return PidUtil.parseAsGlobalID(deaccessionLink); } public Date getCreateTime() { @@ -1367,17 +1389,14 @@ public List getUniqueGrantAgencyValues() { } /** - * @return String containing the version's series title + * @return List of Strings containing the version's series title(s) */ - public String getSeriesTitle() { + public List getSeriesTitles() { List seriesNames = getCompoundChildFieldValues(DatasetFieldConstant.series, DatasetFieldConstant.seriesName); - if (seriesNames.size() > 1) { - logger.warning("More than one series title found for datasetVersion: " + this.id); - } if (!seriesNames.isEmpty()) { - return seriesNames.get(0); + return seriesNames; } else { return null; } @@ -1689,8 +1708,6 @@ public String getSemanticVersion() { public List> validateRequired() { List> returnListreturnList = new ArrayList<>(); - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - Validator validator = factory.getValidator(); for (DatasetField dsf : this.getFlatDatasetFields()) { dsf.setValidationMessage(null); // clear out any existing validation message Set> constraintViolations = validator.validate(dsf); @@ -1704,11 +1721,13 @@ public List> validateRequired() { return returnListreturnList; } + public boolean isValid() { + return validate().isEmpty(); + } + public Set validate() { Set returnSet = new HashSet<>(); - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - Validator validator = factory.getValidator(); for (DatasetField dsf : this.getFlatDatasetFields()) { dsf.setValidationMessage(null); // clear out any existing validation message @@ -2039,10 +2058,8 @@ public String getJsonLd() { for (FileMetadata fileMetadata : fileMetadatasSorted) { JsonObjectBuilder fileObject = NullSafeJsonBuilder.jsonObjectBuilder(); String filePidUrlAsString = null; - URL filePidUrl = fileMetadata.getDataFile().getGlobalId().toURL(); - if (filePidUrl != null) { - filePidUrlAsString = filePidUrl.toString(); - } + GlobalId gid = fileMetadata.getDataFile().getGlobalId(); + filePidUrlAsString = gid != null ? gid.asURL() : null; fileObject.add("@type", "DataDownload"); fileObject.add("name", fileMetadata.getLabel()); fileObject.add("encodingFormat", fileMetadata.getDataFile().getContentType()); diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetVersionDifference.java b/src/main/java/edu/harvard/iq/dataverse/DatasetVersionDifference.java index e844a3f1ca8..eca0c84ae84 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetVersionDifference.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetVersionDifference.java @@ -2,28 +2,29 @@ import edu.harvard.iq.dataverse.datavariable.DataVariable; import edu.harvard.iq.dataverse.datavariable.VarGroup; -import edu.harvard.iq.dataverse.datavariable.VariableMetadata; import edu.harvard.iq.dataverse.datavariable.VariableMetadataUtil; import edu.harvard.iq.dataverse.util.StringUtil; import java.util.ArrayList; import java.util.Collections; -import java.util.Collection; import java.util.List; import java.util.Set; +import java.util.logging.Logger; import org.apache.commons.lang3.StringUtils; import edu.harvard.iq.dataverse.util.BundleUtil; -import edu.harvard.iq.dataverse.util.FileUtil; - import java.util.Arrays; import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; /** * * @author skraffmiller */ public final class DatasetVersionDifference { + private static final Logger logger = Logger.getLogger(DatasetVersionDifference.class.getCanonicalName()); private DatasetVersion newVersion; private DatasetVersion originalVersion; @@ -1713,4 +1714,109 @@ public void setDatasetFilesDiffList(List datasetFiles this.datasetFilesDiffList = datasetFilesDiffList; } + /* + * Static methods to compute which blocks have changes between the two + * DatasetVersions. Currently used to assess whether 'system metadatablocks' + * (protected by a separate key) have changed. (Simplified from the methods + * above that track all the individual changes) + * + */ + public static Set getBlocksWithChanges(DatasetVersion newVersion, DatasetVersion originalVersion) { + Set changedBlockSet = new HashSet(); + + // Compare Data + List newDatasetFields = new LinkedList(newVersion.getDatasetFields()); + if (originalVersion == null) { + // Every field is new, just list blocks used + Iterator dsfnIter = newDatasetFields.listIterator(); + while (dsfnIter.hasNext()) { + DatasetField dsfn = dsfnIter.next(); + if (!changedBlockSet.contains(dsfn.getDatasetFieldType().getMetadataBlock())) { + changedBlockSet.add(dsfn.getDatasetFieldType().getMetadataBlock()); + } + } + + } else { + List originalDatasetFields = new LinkedList(originalVersion.getDatasetFields()); + Iterator dsfoIter = originalDatasetFields.listIterator(); + while (dsfoIter.hasNext()) { + DatasetField dsfo = dsfoIter.next(); + boolean deleted = true; + Iterator dsfnIter = newDatasetFields.listIterator(); + + while (dsfnIter.hasNext()) { + DatasetField dsfn = dsfnIter.next(); + if (dsfo.getDatasetFieldType().equals(dsfn.getDatasetFieldType())) { + deleted = false; + if (!changedBlockSet.contains(dsfo.getDatasetFieldType().getMetadataBlock())) { + logger.fine("Checking " + dsfo.getDatasetFieldType().getName()); + if (dsfo.getDatasetFieldType().isPrimitive()) { + if (fieldsAreDifferent(dsfo, dsfn, false)) { + logger.fine("Adding block for " + dsfo.getDatasetFieldType().getName()); + changedBlockSet.add(dsfo.getDatasetFieldType().getMetadataBlock()); + } + } else { + if (fieldsAreDifferent(dsfo, dsfn, true)) { + logger.fine("Adding block for " + dsfo.getDatasetFieldType().getName()); + changedBlockSet.add(dsfo.getDatasetFieldType().getMetadataBlock()); + } + } + } + dsfnIter.remove(); + break; // if found go to next dataset field + } + } + + if (deleted) { + logger.fine("Adding block for deleted " + dsfo.getDatasetFieldType().getName()); + changedBlockSet.add(dsfo.getDatasetFieldType().getMetadataBlock()); + } + dsfoIter.remove(); + } + // Only fields left are non-matching ones but they may be empty + for (DatasetField dsfn : newDatasetFields) { + if (!dsfn.isEmpty()) { + logger.fine("Adding block for added " + dsfn.getDatasetFieldType().getName()); + changedBlockSet.add(dsfn.getDatasetFieldType().getMetadataBlock()); + } + } + } + return changedBlockSet; + } + + private static boolean fieldsAreDifferent(DatasetField originalField, DatasetField newField, boolean compound) { + String originalValue = ""; + String newValue = ""; + + if (compound) { + for (DatasetFieldCompoundValue datasetFieldCompoundValueOriginal : originalField + .getDatasetFieldCompoundValues()) { + int loopIndex = 0; + if (newField.getDatasetFieldCompoundValues().size() >= loopIndex + 1) { + for (DatasetField dsfo : datasetFieldCompoundValueOriginal.getChildDatasetFields()) { + if (!dsfo.getDisplayValue().isEmpty()) { + originalValue += dsfo.getDisplayValue() + ", "; + } + } + for (DatasetField dsfn : newField.getDatasetFieldCompoundValues().get(loopIndex) + .getChildDatasetFields()) { + if (!dsfn.getDisplayValue().isEmpty()) { + newValue += dsfn.getDisplayValue() + ", "; + } + } + if (!originalValue.trim().equals(newValue.trim())) { + return true; + } + } + loopIndex++; + } + } else { + originalValue = originalField.getDisplayValue(); + newValue = newField.getDisplayValue(); + if (!originalValue.equalsIgnoreCase(newValue)) { + return true; + } + } + return false; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetVersionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetVersionServiceBean.java index 23fc1961b7d..607c46d3662 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetVersionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetVersionServiceBean.java @@ -2,6 +2,7 @@ import edu.harvard.iq.dataverse.DatasetVersion.VersionState; import edu.harvard.iq.dataverse.ingest.IngestUtil; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; import edu.harvard.iq.dataverse.search.IndexServiceBean; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; @@ -153,6 +154,21 @@ public DatasetVersion getDatasetVersion(){ public DatasetVersion find(Object pk) { return em.find(DatasetVersion.class, pk); } + + public DatasetVersion findDeep(Object pk) { + return (DatasetVersion) em.createNamedQuery("DatasetVersion.findById") + .setParameter("id", pk) + // Optimization hints: retrieve all data in one query; this prevents point queries when iterating over the files + .setHint("eclipselink.left-join-fetch", "o.fileMetadatas.dataFile.ingestRequest") + .setHint("eclipselink.left-join-fetch", "o.fileMetadatas.dataFile.thumbnailForDataset") + .setHint("eclipselink.left-join-fetch", "o.fileMetadatas.dataFile.dataTables") + .setHint("eclipselink.left-join-fetch", "o.fileMetadatas.fileCategories") + .setHint("eclipselink.left-join-fetch", "o.fileMetadatas.dataFile.embargo") + .setHint("eclipselink.left-join-fetch", "o.fileMetadatas.datasetVersion") + .setHint("eclipselink.left-join-fetch", "o.fileMetadatas.dataFile.releaseUser") + .setHint("eclipselink.left-join-fetch", "o.fileMetadatas.dataFile.creator") + .getSingleResult(); + } public DatasetVersion findByFriendlyVersionNumber(Long datasetId, String friendlyVersionNumber) { Long majorVersionNumber = null; @@ -559,7 +575,7 @@ public RetrieveDatasetVersionResponse retrieveDatasetVersionByPersistentId(Strin */ GlobalId parsedId; try{ - parsedId = new GlobalId(persistentId); // [ protocol, authority, identifier] + parsedId = PidUtil.parseAsGlobalID(persistentId); // [ protocol, authority, identifier] } catch (IllegalArgumentException e){ logger.log(Level.WARNING, "Failed to parse persistentID: {0}", persistentId); return null; @@ -892,7 +908,7 @@ public void populateDatasetSearchCard(SolrSearchResult solrSearchResult) { if (searchResult.length == 5) { Dataset datasetEntity = new Dataset(); String globalIdentifier = solrSearchResult.getIdentifier(); - GlobalId globalId = new GlobalId(globalIdentifier); + GlobalId globalId = PidUtil.parseAsGlobalID(globalIdentifier); datasetEntity.setProtocol(globalId.getProtocol()); datasetEntity.setAuthority(globalId.getAuthority()); @@ -1117,13 +1133,7 @@ public JsonObjectBuilder fixMissingUnf(String datasetVersionId, boolean forceRec // reindexing the dataset, to make sure the new UNF is in SOLR: boolean doNormalSolrDocCleanUp = true; - try { - Future indexingResult = indexService.indexDataset(datasetVersion.getDataset(), doNormalSolrDocCleanUp); - } catch (IOException | SolrServerException e) { - String failureLogText = "Post UNF update indexing failed. You can kickoff a re-index of this dataset with: \r\n curl http://localhost:8080/api/admin/index/datasets/" + datasetVersion.getDataset().getId().toString(); - failureLogText += "\r\n" + e.getLocalizedMessage(); - LoggingUtil.writeOnSuccessFailureLog(null, failureLogText, datasetVersion.getDataset()); - } + indexService.asyncIndexDataset(datasetVersion.getDataset(), doNormalSolrDocCleanUp); return info; } diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetVersionUI.java b/src/main/java/edu/harvard/iq/dataverse/DatasetVersionUI.java index d09457c86bf..6e9f9c17f7a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetVersionUI.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetVersionUI.java @@ -6,23 +6,18 @@ package edu.harvard.iq.dataverse; import edu.harvard.iq.dataverse.util.MarkupChecker; -import edu.harvard.iq.dataverse.util.StringUtil; import java.io.Serializable; -import java.sql.Timestamp; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.Date; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.TreeMap; -import static java.util.stream.Collectors.toList; import javax.ejb.EJB; import javax.faces.view.ViewScoped; -import javax.inject.Named; +import javax.inject.Inject; +import javax.json.JsonObject; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; @@ -35,6 +30,9 @@ public class DatasetVersionUI implements Serializable { @EJB DataverseServiceBean dataverseService; + @Inject + SettingsWrapper settingsWrapper; + @PersistenceContext(unitName = "VDCNet-ejbPU") private EntityManager em; @@ -400,6 +398,9 @@ public void setMetadataValueBlocks(DatasetVersion datasetVersion) { //TODO: A lot of clean up on the logic of this method metadataBlocksForView.clear(); metadataBlocksForEdit.clear(); + + List systemMDBlocks = settingsWrapper.getSystemMetadataBlocks(); + Long dvIdForInputLevel = datasetVersion.getDataset().getOwner().getId(); if (!dataverseService.find(dvIdForInputLevel).isMetadataBlockRoot()){ @@ -442,7 +443,7 @@ public void setMetadataValueBlocks(DatasetVersion datasetVersion) { if (!datasetFieldsForView.isEmpty()) { metadataBlocksForView.put(mdb, datasetFieldsForView); } - if (!datasetFieldsForEdit.isEmpty()) { + if (!datasetFieldsForEdit.isEmpty() && !systemMDBlocks.contains(mdb)) { metadataBlocksForEdit.put(mdb, datasetFieldsForEdit); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetWidgetsPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetWidgetsPage.java index 9cc611e146a..9c47a58811a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetWidgetsPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetWidgetsPage.java @@ -164,7 +164,7 @@ public String save() { try { DatasetThumbnail datasetThumbnailFromCommand = commandEngine.submit(updateDatasetThumbnailCommand); JsfHelper.addSuccessMessage(BundleUtil.getStringFromBundle("dataset.thumbnailsAndWidget.success")); - return "/dataset.xhtml?persistentId=" + dataset.getGlobalIdString() + "&faces-redirect=true"; + return "/dataset.xhtml?persistentId=" + dataset.getGlobalId().asString() + "&faces-redirect=true"; } catch (CommandException ex) { String error = ex.getLocalizedMessage(); /** @@ -179,7 +179,7 @@ public String save() { public String cancel() { logger.fine("cancel clicked"); - return "/dataset.xhtml?persistentId=" + dataset.getGlobalIdString() + "&faces-redirect=true"; + return "/dataset.xhtml?persistentId=" + dataset.getGlobalId().asString() + "&faces-redirect=true"; } } diff --git a/src/main/java/edu/harvard/iq/dataverse/Dataverse.java b/src/main/java/edu/harvard/iq/dataverse/Dataverse.java index bc8716b6129..50d5ae09548 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Dataverse.java +++ b/src/main/java/edu/harvard/iq/dataverse/Dataverse.java @@ -590,8 +590,34 @@ public void setCitationDatasetFieldTypes(List citationDatasetF this.citationDatasetFieldTypes = citationDatasetFieldTypes; } - + /** + * @Note: this setting is Nullable, with {@code null} indicating that the + * desired behavior is not explicitly configured for this specific collection. + * See the comment below. + */ + @Column(nullable = true) + private Boolean filePIDsEnabled; + /** + * Specifies whether the PIDs for Datafiles should be registered when publishing + * datasets in this Collection, if the behavior is explicitly configured. + * @return {@code Boolean.TRUE} if explicitly enabled, {@code Boolean.FALSE} if explicitly disabled. + * {@code null} indicates that the behavior is not explicitly defined, in which + * case the behavior should follow the explicit configuration of the first + * direct ancestor collection, or the instance-wide configuration, if none + * present. + * @Note: If present, this configuration therefore by default applies to all + * the sub-collections, unless explicitly overwritten there. + * @author landreev + */ + public Boolean getFilePIDsEnabled() { + return filePIDsEnabled; + } + + public void setFilePIDsEnabled(boolean filePIDsEnabled) { + this.filePIDsEnabled = filePIDsEnabled; + } + public List getDataverseFacets() { return getDataverseFacets(false); } diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java index 9d09d0580e2..b83593f5b6e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java @@ -303,7 +303,7 @@ public Set availableRoles(Long dvId) { Set roles = dv.getRoles(); roles.addAll(findBuiltinRoles()); - while (!dv.isEffectivelyPermissionRoot()) { + while (dv.getOwner() != null) { dv = dv.getOwner(); roles.addAll(dv.getRoles()); } diff --git a/src/main/java/edu/harvard/iq/dataverse/DvObject.java b/src/main/java/edu/harvard/iq/dataverse/DvObject.java index 09a2ef85893..e3013b8cf51 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DvObject.java +++ b/src/main/java/edu/harvard/iq/dataverse/DvObject.java @@ -1,6 +1,8 @@ package edu.harvard.iq.dataverse; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; + import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.util.Arrays; @@ -8,6 +10,8 @@ import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.logging.Logger; + import javax.persistence.*; /** @@ -26,9 +30,13 @@ query="SELECT COUNT(obj) FROM DvObject obj WHERE obj.owner.id=:id"), @NamedQuery(name = "DvObject.findByGlobalId", query = "SELECT o FROM DvObject o WHERE o.identifier=:identifier and o.authority=:authority and o.protocol=:protocol and o.dtype=:dtype"), + @NamedQuery(name = "DvObject.findIdByGlobalId", + query = "SELECT o.id FROM DvObject o WHERE o.identifier=:identifier and o.authority=:authority and o.protocol=:protocol and o.dtype=:dtype"), @NamedQuery(name = "DvObject.findByAlternativeGlobalId", query = "SELECT o FROM DvObject o, AlternativePersistentIdentifier a WHERE o.id = a.dvObject.id and a.identifier=:identifier and a.authority=:authority and a.protocol=:protocol and o.dtype=:dtype"), + @NamedQuery(name = "DvObject.findIdByAlternativeGlobalId", + query = "SELECT o.id FROM DvObject o, AlternativePersistentIdentifier a WHERE o.id = a.dvObject.id and a.identifier=:identifier and a.authority=:authority and a.protocol=:protocol and o.dtype=:dtype"), @NamedQuery(name = "DvObject.findByProtocolIdentifierAuthority", query = "SELECT o FROM DvObject o WHERE o.identifier=:identifier and o.authority=:authority and o.protocol=:protocol"), @@ -51,10 +59,19 @@ uniqueConstraints = {@UniqueConstraint(columnNames = {"authority,protocol,identifier"}),@UniqueConstraint(columnNames = {"owner_id,storageidentifier"})}) public abstract class DvObject extends DataverseEntity implements java.io.Serializable { - public static final String DATAVERSE_DTYPE_STRING = "Dataverse"; - public static final String DATASET_DTYPE_STRING = "Dataset"; - public static final String DATAFILE_DTYPE_STRING = "DataFile"; - public static final List DTYPE_LIST = Arrays.asList(DATAVERSE_DTYPE_STRING, DATASET_DTYPE_STRING, DATAFILE_DTYPE_STRING); + private static final Logger logger = Logger.getLogger(DvObject.class.getCanonicalName()); + + public enum DType { + Dataverse("Dataverse"), Dataset("Dataset"),DataFile("DataFile"); + + String dtype; + DType(String dt) { + dtype = dt; + } + public String getDType() { + return dtype; + } + } public static final Visitor NamePrinter = new Visitor(){ @@ -140,6 +157,8 @@ public String visit(DataFile df) { private boolean identifierRegistered; + private transient GlobalId globalId = null; + @OneToMany(mappedBy = "dvObject", cascade = CascadeType.ALL, orphanRemoval = true) private Set alternativePersistentIndentifiers; @@ -272,6 +291,8 @@ public String getProtocol() { public void setProtocol(String protocol) { this.protocol = protocol; + //Remove cached value + globalId=null; } public String getAuthority() { @@ -280,6 +301,8 @@ public String getAuthority() { public void setAuthority(String authority) { this.authority = authority; + //Remove cached value + globalId=null; } public Date getGlobalIdCreateTime() { @@ -296,6 +319,8 @@ public String getIdentifier() { public void setIdentifier(String identifier) { this.identifier = identifier; + //Remove cached value + globalId=null; } public boolean isIdentifierRegistered() { @@ -306,22 +331,13 @@ public void setIdentifierRegistered(boolean identifierRegistered) { this.identifierRegistered = identifierRegistered; } - /** - * - * @return This object's global id in a string form. - * @deprecated use {@code dvobj.getGlobalId().asString()}. - */ - public String getGlobalIdString() { - final GlobalId globalId = getGlobalId(); - return globalId != null ? globalId.asString() : null; - } - public void setGlobalId( GlobalId pid ) { if ( pid == null ) { setProtocol(null); setAuthority(null); setIdentifier(null); } else { + //These reset globalId=null setProtocol(pid.getProtocol()); setAuthority(pid.getAuthority()); setIdentifier(pid.getIdentifier()); @@ -329,10 +345,11 @@ public void setGlobalId( GlobalId pid ) { } public GlobalId getGlobalId() { - // FIXME should return NULL when the fields are null. Currenntly, - // a lot of code depends call this method, so this fix can't be - // a part of the current PR. - return new GlobalId(getProtocol(), getAuthority(), getIdentifier()); + // Cache this + if ((globalId == null) && !(getProtocol() == null || getAuthority() == null || getIdentifier() == null)) { + globalId = PidUtil.parseAsGlobalID(getProtocol(), getAuthority(), getIdentifier()); + } + return globalId; } public abstract T accept(Visitor v); @@ -420,17 +437,7 @@ public String getAuthorString(){ } public String getTargetUrl(){ - if (this instanceof Dataverse){ - throw new UnsupportedOperationException("Not supported yet."); - } - if (this instanceof Dataset){ - return Dataset.TARGET_URL; - } - if (this instanceof DataFile){ - return DataFile.TARGET_URL; - } throw new UnsupportedOperationException("Not supported yet. New DVObject Instance?"); - } public String getYearPublishedCreated(){ diff --git a/src/main/java/edu/harvard/iq/dataverse/DvObjectServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DvObjectServiceBean.java index 01b0890d588..c9127af7c2b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DvObjectServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DvObjectServiceBean.java @@ -1,6 +1,8 @@ package edu.harvard.iq.dataverse; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; + import java.sql.Timestamp; import java.util.ArrayList; import java.util.Date; @@ -19,6 +21,8 @@ import javax.persistence.NonUniqueResultException; import javax.persistence.PersistenceContext; import javax.persistence.Query; +import javax.persistence.StoredProcedureQuery; + import org.apache.commons.lang3.StringUtils; import org.ocpsoft.common.util.Strings; @@ -79,46 +83,108 @@ public boolean checkExists(Long id) { Long result =(Long)query.getSingleResult(); return result > 0; } - // FIXME This type-by-string has to go, in favor of passing a class parameter. - public DvObject findByGlobalId(String globalIdString, String typeString) { - return findByGlobalId(globalIdString, typeString, false); + + public DvObject findByGlobalId(String globalIdString, DvObject.DType dtype) { + try { + GlobalId gid = PidUtil.parseAsGlobalID(globalIdString); + return findByGlobalId(gid, dtype); + } catch (IllegalArgumentException iae) { + logger.fine("Invalid identifier: " + globalIdString); + return null; + } + } - // FIXME This type-by-string has to go, in favor of passing a class parameter. - public DvObject findByGlobalId(String globalIdString, String typeString, Boolean altId) { - + public DvObject findByAltGlobalId(String globalIdString, DvObject.DType dtype) { try { - GlobalId gid = new GlobalId(globalIdString); + GlobalId gid = PidUtil.parseAsGlobalID(globalIdString); + return findByAltGlobalId(gid, dtype); + } catch (IllegalArgumentException iae) { + logger.fine("Invalid alternate identifier: " + globalIdString); + return null; + } - DvObject foundDvObject = null; - try { - Query query; - if (altId) { - query = em.createNamedQuery("DvObject.findByAlternativeGlobalId"); - } else{ - query = em.createNamedQuery("DvObject.findByGlobalId"); - } - query.setParameter("identifier", gid.getIdentifier()); - query.setParameter("protocol", gid.getProtocol()); - query.setParameter("authority", gid.getAuthority()); - query.setParameter("dtype", typeString); - foundDvObject = (DvObject) query.getSingleResult(); - } catch (javax.persistence.NoResultException e) { - // (set to .info, this can fill the log file with thousands of - // these messages during a large harvest run) - logger.fine("no dvObject found: " + globalIdString); - // DO nothing, just return null. - return null; - } catch (Exception ex) { - logger.info("Exception caught in findByGlobalId: " + ex.getLocalizedMessage()); - return null; - } - return foundDvObject; + } - } catch (IllegalArgumentException iae) { - logger.info("Invalid identifier: " + globalIdString); + public DvObject findByGlobalId(GlobalId globalId, DvObject.DType dtype) { + Query query = em.createNamedQuery("DvObject.findByGlobalId"); + return runFindByGlobalId(query, globalId, dtype); + } + + public DvObject findByAltGlobalId(GlobalId globalId, DvObject.DType dtype) { + Query query = em.createNamedQuery("DvObject.findByAlternativeGlobalId"); + return runFindByGlobalId(query, globalId, dtype); + } + + public Long findIdByGlobalId(GlobalId globalId, DvObject.DType dtype) { + Query query = em.createNamedQuery("DvObject.findIdByGlobalId"); + return runFindIdByGlobalId(query, globalId, dtype); + } + + public Long findIdByAltGlobalId(GlobalId globalId, DvObject.DType dtype) { + Query query = em.createNamedQuery("DvObject.findIdByAlternativeGlobalId"); + return runFindIdByGlobalId(query, globalId, dtype); + } + + private DvObject runFindByGlobalId(Query query, GlobalId gid, DvObject.DType dtype) { + DvObject foundDvObject = null; + try { + query.setParameter("identifier", gid.getIdentifier()); + query.setParameter("protocol", gid.getProtocol()); + query.setParameter("authority", gid.getAuthority()); + query.setParameter("dtype", dtype.getDType()); + foundDvObject = (DvObject) query.getSingleResult(); + } catch (javax.persistence.NoResultException e) { + // (set to .info, this can fill the log file with thousands of + // these messages during a large harvest run) + logger.fine("no dvObject found: " + gid.asString()); + // DO nothing, just return null. + return null; + } catch (Exception ex) { + logger.info("Exception caught in findByGlobalId: " + ex.getLocalizedMessage()); + return null; + } + return foundDvObject; + } + + private Long runFindIdByGlobalId(Query query, GlobalId gid, DvObject.DType dtype) { + Long foundDvObject = null; + try { + query.setParameter("identifier", gid.getIdentifier()); + query.setParameter("protocol", gid.getProtocol()); + query.setParameter("authority", gid.getAuthority()); + query.setParameter("dtype", dtype.getDType()); + foundDvObject = (Long) query.getSingleResult(); + } catch (javax.persistence.NoResultException e) { + // (set to .info, this can fill the log file with thousands of + // these messages during a large harvest run) + logger.fine("no dvObject found: " + gid.asString()); + // DO nothing, just return null. + return null; + } catch (Exception ex) { + logger.info("Exception caught in findByGlobalId: " + ex.getLocalizedMessage()); return null; } + return foundDvObject; + } + + public DvObject findByGlobalId(GlobalId globalId) { + try { + return (DvObject) em.createNamedQuery("DvObject.findByProtocolIdentifierAuthority") + .setParameter("identifier", globalId.getIdentifier()) + .setParameter("authority", globalId.getAuthority()).setParameter("protocol", globalId.getProtocol()) + .getSingleResult(); + } catch (NoResultException nre) { + return null; + } + } + + public boolean isGlobalIdLocallyUnique(GlobalId globalId) { + return em.createNamedQuery("DvObject.findByProtocolIdentifierAuthority") + .setParameter("identifier", globalId.getIdentifier()) + .setParameter("authority", globalId.getAuthority()) + .setParameter("protocol", globalId.getProtocol()) + .getResultList().isEmpty(); } public DvObject updateContentIndexTime(DvObject dvObject) { @@ -317,4 +383,11 @@ public Map getObjectPathsByIds(Set objectIds){ } return ret; } + + public String generateNewIdentifierByStoredProcedure() { + StoredProcedureQuery query = this.em.createNamedStoredProcedureQuery("Dataset.generateIdentifierFromStoredProcedure"); + query.execute(); + return (String) query.getOutputParameterValue(1); + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java b/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java index b4efe7ec41d..7185887ecc3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java +++ b/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java @@ -18,6 +18,7 @@ import edu.harvard.iq.dataverse.engine.command.exception.PermissionException; import edu.harvard.iq.dataverse.ingest.IngestServiceBean; import edu.harvard.iq.dataverse.pidproviders.FakePidProviderServiceBean; +import edu.harvard.iq.dataverse.pidproviders.PermaLinkPidProviderServiceBean; import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean; import edu.harvard.iq.dataverse.search.IndexBatchServiceBean; import edu.harvard.iq.dataverse.search.IndexServiceBean; @@ -124,6 +125,9 @@ public class EjbDataverseEngine { @EJB HandlenetServiceBean handleNet; + @EJB + PermaLinkPidProviderServiceBean permaLinkProvider; + @EJB SettingsServiceBean settings; @@ -496,6 +500,11 @@ public HandlenetServiceBean handleNet() { return handleNet; } + @Override + public PermaLinkPidProviderServiceBean permaLinkProvider() { + return permaLinkProvider; + } + @Override public SettingsServiceBean settings() { return settings; diff --git a/src/main/java/edu/harvard/iq/dataverse/FileAccessRequest.java b/src/main/java/edu/harvard/iq/dataverse/FileAccessRequest.java new file mode 100644 index 00000000000..76c5df4409a --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/FileAccessRequest.java @@ -0,0 +1,91 @@ +package edu.harvard.iq.dataverse; + +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; + +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.MapsId; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.io.Serializable; +import java.util.Date; + +@Entity +@Table(name = "fileaccessrequests") +public class FileAccessRequest { + @EmbeddedId + private FileAccessRequestKey id; + @ManyToOne + @MapsId("dataFile") + @JoinColumn(name = "datafile_id") + private DataFile dataFile; + @ManyToOne + @MapsId("authenticatedUser") + @JoinColumn(name = "authenticated_user_id") + private AuthenticatedUser authenticatedUser; + + @Temporal(value = TemporalType.TIMESTAMP) + @Column(name = "creation_time") + private Date creationTime; + + public FileAccessRequestKey getId() { + return id; + } + + public void setId(FileAccessRequestKey id) { + this.id = id; + } + + public DataFile getDataFile() { + return dataFile; + } + + public void setDataFile(DataFile dataFile) { + this.dataFile = dataFile; + } + + public AuthenticatedUser getAuthenticatedUser() { + return authenticatedUser; + } + + public void setAuthenticatedUser(AuthenticatedUser authenticatedUser) { + this.authenticatedUser = authenticatedUser; + } + + public Date getCreationTime() { + return creationTime; + } + + public void setCreationTime(Date creationTime) { + this.creationTime = creationTime; + } + + @Embeddable + public static class FileAccessRequestKey implements Serializable { + @Column(name = "datafile_id") + private Long dataFile; + @Column(name = "authenticated_user_id") + private Long authenticatedUser; + + public Long getDataFile() { + return dataFile; + } + + public void setDataFile(Long dataFile) { + this.dataFile = dataFile; + } + + public Long getAuthenticatedUser() { + return authenticatedUser; + } + + public void setAuthenticatedUser(Long authenticatedUser) { + this.authenticatedUser = authenticatedUser; + } + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/FileDownloadHelper.java b/src/main/java/edu/harvard/iq/dataverse/FileDownloadHelper.java index ef7ed1a2010..850efc2f1ae 100644 --- a/src/main/java/edu/harvard/iq/dataverse/FileDownloadHelper.java +++ b/src/main/java/edu/harvard/iq/dataverse/FileDownloadHelper.java @@ -324,13 +324,12 @@ public void requestAccessIndirect() { private boolean processRequestAccess(DataFile file, Boolean sendNotification) { if (fileDownloadService.requestAccess(file.getId())) { // update the local file object so that the page properly updates - if(file.getFileAccessRequesters() == null){ - file.setFileAccessRequesters(new ArrayList()); - } - file.getFileAccessRequesters().add((AuthenticatedUser) session.getUser()); + AuthenticatedUser user = (AuthenticatedUser) session.getUser(); + file.addFileAccessRequester(user); + // create notification if necessary if (sendNotification) { - fileDownloadService.sendRequestFileAccessNotification(file, (AuthenticatedUser) session.getUser()); + fileDownloadService.sendRequestFileAccessNotification(file, user); } JsfHelper.addSuccessMessage(BundleUtil.getStringFromBundle("file.accessRequested.success")); return true; diff --git a/src/main/java/edu/harvard/iq/dataverse/FileDownloadServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/FileDownloadServiceBean.java index 65e6b259bf4..a90489be29a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/FileDownloadServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/FileDownloadServiceBean.java @@ -299,7 +299,7 @@ public void explore(GuestbookResponse guestbookResponse, FileMetadata fmd, Exter ApiToken apiToken = null; User user = session.getUser(); DatasetVersion version = fmd.getDatasetVersion(); - if (version.isDraft() || (fmd.getDataFile().isRestricted()) || (FileUtil.isActivelyEmbargoed(fmd))) { + if (version.isDraft() || fmd.getDatasetVersion().isDeaccessioned() || (fmd.getDataFile().isRestricted()) || (FileUtil.isActivelyEmbargoed(fmd))) { apiToken = getApiToken(user); } DataFile dataFile = null; @@ -489,7 +489,7 @@ public boolean requestAccess(Long fileId) { return false; } DataFile file = datafileService.find(fileId); - if (!file.getFileAccessRequesters().contains((AuthenticatedUser)session.getUser())) { + if (!file.containsFileAccessRequestFromUser(session.getUser())) { try { commandEngine.submit(new RequestAccessCommand(dvRequestService.getDataverseRequest(), file)); return true; diff --git a/src/main/java/edu/harvard/iq/dataverse/FileMetadata.java b/src/main/java/edu/harvard/iq/dataverse/FileMetadata.java index fc31d0867ed..01131bdca01 100644 --- a/src/main/java/edu/harvard/iq/dataverse/FileMetadata.java +++ b/src/main/java/edu/harvard/iq/dataverse/FileMetadata.java @@ -13,10 +13,13 @@ import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.Date; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.json.Json; @@ -203,6 +206,25 @@ public void setVarGroups(List varGroups) { private List fileCategories; public List getCategories() { + if (fileCategories != null) { + /* + * fileCategories can sometimes be an + * org.eclipse.persistence.indirection.IndirectList When that happens, the + * comparator in the Collections.sort below is not called, possibly due to + * https://bugs.eclipse.org/bugs/show_bug.cgi?id=446236 which is Java 1.8+ + * specific Converting to an ArrayList solves the problem, but the longer term + * solution may be in avoiding the IndirectList or moving to a new version of + * the jar it is in. + */ + if (!(fileCategories instanceof ArrayList)) { + List newDFCs = new ArrayList(); + for (DataFileCategory fdc : fileCategories) { + newDFCs.add(fdc); + } + setCategories(newDFCs); + } + Collections.sort(fileCategories, FileMetadata.compareByNameWithSortCategories); + } return fileCategories; } @@ -228,7 +250,7 @@ public List getCategoriesByName() { return ret; } - for (DataFileCategory fileCategory : fileCategories) { + for (DataFileCategory fileCategory : getCategories()) { ret.add(fileCategory.getName()); } // fileCategories.stream() @@ -536,7 +558,7 @@ public boolean compareContent(FileMetadata other){ @Override public String toString() { - return "edu.harvard.iq.dvn.core.study.FileMetadata[id=" + id + "]"; + return "edu.harvard.iq.dataverse.FileMetadata[id=" + id + "]"; } public static final Comparator compareByLabel = new Comparator() { @@ -546,28 +568,37 @@ public int compare(FileMetadata o1, FileMetadata o2) { } }; - public static final Comparator compareByLabelAndFolder = new Comparator() { + static Map categoryMap=null; + + public static void setCategorySortOrder(String categories) { + categoryMap=new HashMap(); + long i=1; + for(String cat: categories.split(",\\s*")) { + categoryMap.put(cat.toUpperCase(), i); + i++; + } + } + + public static Map getCategorySortOrder() { + return categoryMap; + } + + + public static final Comparator compareByNameWithSortCategories = new Comparator() { @Override - public int compare(FileMetadata o1, FileMetadata o2) { - String folder1 = o1.getDirectoryLabel() == null ? "" : o1.getDirectoryLabel().toUpperCase(); - String folder2 = o2.getDirectoryLabel() == null ? "" : o2.getDirectoryLabel().toUpperCase(); - - - // We want to the files w/ no folders appear *after* all the folders - // on the sorted list: - if ("".equals(folder1) && !"".equals(folder2)) { - return 1; - } - - if ("".equals(folder2) && !"".equals(folder1)) { - return -1; - } - - int comp = folder1.compareTo(folder2); - if (comp != 0) { - return comp; + public int compare(DataFileCategory o1, DataFileCategory o2) { + if (categoryMap != null) { + //If one is in the map and one is not, the former is first, otherwise sort by name + boolean o1InMap = categoryMap.containsKey(o1.getName().toUpperCase()); + boolean o2InMap = categoryMap.containsKey(o2.getName().toUpperCase()); + if(o1InMap && !o2InMap) { + return (-1); + } + if(!o1InMap && o2InMap) { + return 1; + } } - return o1.getLabel().toUpperCase().compareTo(o2.getLabel().toUpperCase()); + return(o1.getName().toUpperCase().compareTo(o2.getName().toUpperCase())); } }; diff --git a/src/main/java/edu/harvard/iq/dataverse/FilePage.java b/src/main/java/edu/harvard/iq/dataverse/FilePage.java index 228db0a7584..bee5ce20339 100644 --- a/src/main/java/edu/harvard/iq/dataverse/FilePage.java +++ b/src/main/java/edu/harvard/iq/dataverse/FilePage.java @@ -22,9 +22,9 @@ import edu.harvard.iq.dataverse.engine.command.impl.PersistProvFreeFormCommand; import edu.harvard.iq.dataverse.engine.command.impl.RestrictFileCommand; import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand; -import edu.harvard.iq.dataverse.export.ExportException; import edu.harvard.iq.dataverse.export.ExportService; -import edu.harvard.iq.dataverse.export.spi.Exporter; +import io.gdcc.spi.export.ExportException; +import io.gdcc.spi.export.Exporter; import edu.harvard.iq.dataverse.externaltools.ExternalTool; import edu.harvard.iq.dataverse.externaltools.ExternalToolHandler; import edu.harvard.iq.dataverse.externaltools.ExternalToolServiceBean; @@ -80,6 +80,7 @@ public class FilePage implements java.io.Serializable { private FileMetadata fileMetadata; private Long fileId; private String version; + private String toolType; private DataFile file; private GuestbookResponse guestbookResponse; private int selectedTabIndex; @@ -91,6 +92,7 @@ public class FilePage implements java.io.Serializable { private List configureTools; private List exploreTools; private List toolsWithPreviews; + private List queryTools; private Long datasetVersionId; /** * Have the terms been met so that the Preview tab can show the preview? @@ -152,7 +154,6 @@ public String init() { if (fileId != null || persistentId != null) { - // --------------------------------------- // Set the file and datasetVersion // --------------------------------------- @@ -242,13 +243,28 @@ public String init() { } configureTools = externalToolService.findFileToolsByTypeAndContentType(ExternalTool.Type.CONFIGURE, contentType); exploreTools = externalToolService.findFileToolsByTypeAndContentType(ExternalTool.Type.EXPLORE, contentType); + queryTools = externalToolService.findFileToolsByTypeAndContentType(ExternalTool.Type.QUERY, contentType); Collections.sort(exploreTools, CompareExternalToolName); toolsWithPreviews = sortExternalTools(); - if(!toolsWithPreviews.isEmpty()){ - setSelectedTool(toolsWithPreviews.get(0)); + + if (toolType != null) { + if (toolType.equals("PREVIEW")) { + if (!toolsWithPreviews.isEmpty()) { + setSelectedTool(toolsWithPreviews.get(0)); + } + } + if (toolType.equals("QUERY")) { + if (!queryTools.isEmpty()) { + setSelectedTool(queryTools.get(0)); + } + } + } else { + if (!getAllAvailableTools().isEmpty()){ + setSelectedTool(getAllAvailableTools().get(0)); + } } - } else { + } else { return permissionsWrapper.notFound(); } @@ -266,10 +282,19 @@ public String init() { private void displayPublishMessage(){ if (fileMetadata.getDatasetVersion().isDraft() && canUpdateDataset() && (canPublishDataset() || !fileMetadata.getDatasetVersion().getDataset().isLockedFor(DatasetLock.Reason.InReview))){ - JsfHelper.addWarningMessage(datasetService.getReminderString(fileMetadata.getDatasetVersion().getDataset(), canPublishDataset(), true)); + JsfHelper.addWarningMessage(datasetService.getReminderString(fileMetadata.getDatasetVersion().getDataset(), canPublishDataset(), true, isValid())); } } + public boolean isValid() { + if (!fileMetadata.getDatasetVersion().isDraft()) { + return true; + } + DatasetVersion newVersion = fileMetadata.getDatasetVersion().cloneDatasetVersion(); + newVersion.setDatasetFields(newVersion.initDatasetFields()); + return newVersion.isValid(); + } + private boolean canViewUnpublishedDataset() { return permissionsWrapper.canViewUnpublishedDataset( dvRequestService.getDataverseRequest(), fileMetadata.getDatasetVersion().getDataset()); } @@ -364,9 +389,9 @@ public List< String[]> getExporters(){ // Not all metadata exports should be presented to the web users! // Some are only for harvesting clients. - String[] temp = new String[2]; + String[] temp = new String[2]; temp[0] = formatDisplayName; - temp[1] = myHostURL + "/api/datasets/export?exporter=" + formatName + "&persistentId=" + fileMetadata.getDatasetVersion().getDataset().getGlobalIdString(); + temp[1] = myHostURL + "/api/datasets/export?exporter=" + formatName + "&persistentId=" + fileMetadata.getDatasetVersion().getDataset().getGlobalId().asString(); retList.add(temp); } } @@ -726,7 +751,7 @@ public boolean isThumbnailAvailable(FileMetadata fileMetadata) { private String returnToDatasetOnly(){ - return "/dataset.xhtml?persistentId=" + editDataset.getGlobalIdString() + "&version=DRAFT" + "&faces-redirect=true"; + return "/dataset.xhtml?persistentId=" + editDataset.getGlobalId().asString() + "&version=DRAFT" + "&faces-redirect=true"; } private String returnToDraftVersion(){ @@ -858,9 +883,9 @@ public String getComputeUrl() throws IOException { swiftObject.open(); //generate a temp url for a file if (isHasPublicStore()) { - return settingsService.getValueForKey(SettingsServiceBean.Key.ComputeBaseUrl) + "?" + this.getFile().getOwner().getGlobalIdString() + "=" + swiftObject.getSwiftFileName(); + return settingsService.getValueForKey(SettingsServiceBean.Key.ComputeBaseUrl) + "?" + this.getFile().getOwner().getGlobalId().asString() + "=" + swiftObject.getSwiftFileName(); } - return settingsService.getValueForKey(SettingsServiceBean.Key.ComputeBaseUrl) + "?" + this.getFile().getOwner().getGlobalIdString() + "=" + swiftObject.getSwiftFileName() + "&temp_url_sig=" + swiftObject.getTempUrlSignature() + "&temp_url_expires=" + swiftObject.getTempUrlExpiry(); + return settingsService.getValueForKey(SettingsServiceBean.Key.ComputeBaseUrl) + "?" + this.getFile().getOwner().getGlobalId().asString() + "=" + swiftObject.getSwiftFileName() + "&temp_url_sig=" + swiftObject.getTempUrlSignature() + "&temp_url_expires=" + swiftObject.getTempUrlExpiry(); } return ""; } @@ -983,6 +1008,30 @@ public List getToolsWithPreviews() { return toolsWithPreviews; } + public List getQueryTools() { + return queryTools; + } + + + public List getAllAvailableTools(){ + List externalTools = new ArrayList<>(); + externalTools.addAll(queryTools); + for (ExternalTool pt : toolsWithPreviews){ + if (!externalTools.contains(pt)){ + externalTools.add(pt); + } + } + return externalTools; + } + + public String getToolType() { + return toolType; + } + + public void setToolType(String toolType) { + this.toolType = toolType; + } + private ExternalTool selectedTool; public ExternalTool getSelectedTool() { @@ -996,7 +1045,7 @@ public void setSelectedTool(ExternalTool selectedTool) { public String preview(ExternalTool externalTool) { ApiToken apiToken = null; User user = session.getUser(); - if (fileMetadata.getDatasetVersion().isDraft() || (fileMetadata.getDataFile().isRestricted()) || (FileUtil.isActivelyEmbargoed(fileMetadata))) { + if (fileMetadata.getDatasetVersion().isDraft() || fileMetadata.getDatasetVersion().isDeaccessioned() || (fileMetadata.getDataFile().isRestricted()) || (FileUtil.isActivelyEmbargoed(fileMetadata))) { apiToken=fileDownloadService.getApiToken(user); } if(externalTool == null){ @@ -1175,7 +1224,22 @@ public String getEmbargoPhrase() { return BundleUtil.getStringFromBundle("embargoed.willbeuntil"); } } - + + public String getToolTabTitle(){ + if (getAllAvailableTools().size() > 1) { + return BundleUtil.getStringFromBundle("file.toolTab.header"); + } + if( getSelectedTool() != null ){ + if(getSelectedTool().isPreviewTool()){ + return BundleUtil.getStringFromBundle("file.previewTab.header"); + } + if(getSelectedTool().isQueryTool()){ + return BundleUtil.getStringFromBundle("file.queryTab.header"); + } + } + return BundleUtil.getStringFromBundle("file.toolTab.header"); + } + public String getIngestMessage() { return BundleUtil.getStringFromBundle("file.ingestFailed.message", Arrays.asList(settingsWrapper.getGuidesBaseUrl(), settingsWrapper.getGuidesVersion())); } diff --git a/src/main/java/edu/harvard/iq/dataverse/GlobalId.java b/src/main/java/edu/harvard/iq/dataverse/GlobalId.java index 20b280771fc..890b146a61c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/GlobalId.java +++ b/src/main/java/edu/harvard/iq/dataverse/GlobalId.java @@ -6,7 +6,7 @@ package edu.harvard.iq.dataverse; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.pidproviders.PermaLinkPidProviderServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; import static edu.harvard.iq.dataverse.util.StringUtil.isEmpty; import java.net.MalformedURLException; @@ -16,7 +16,6 @@ import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.ejb.EJB; /** * @@ -24,55 +23,28 @@ */ public class GlobalId implements java.io.Serializable { - public static final String DOI_PROTOCOL = "doi"; - public static final String HDL_PROTOCOL = "hdl"; - public static final String DOI_RESOLVER_URL = "https://doi.org/"; - public static final String DXDOI_RESOLVER_URL = "https://dx.doi.org/"; - public static final String HDL_RESOLVER_URL = "https://hdl.handle.net/"; - public static final String HTTP_DOI_RESOLVER_URL = "http://doi.org/"; - public static final String HTTP_DXDOI_RESOLVER_URL = "http://dx.doi.org/"; - public static final String HTTP_HDL_RESOLVER_URL = "http://hdl.handle.net/"; - - public static Optional parse(String identifierString) { - try { - return Optional.of(new GlobalId(identifierString)); - } catch ( IllegalArgumentException _iae) { - return Optional.empty(); - } - } - private static final Logger logger = Logger.getLogger(GlobalId.class.getName()); - - @EJB - SettingsServiceBean settingsService; - /** - * - * @param identifier The string to be parsed - * @throws IllegalArgumentException if the passed string cannot be parsed. - */ - public GlobalId(String identifier) { - // set the protocol, authority, and identifier via parsePersistentId - if ( ! parsePersistentId(identifier) ){ - throw new IllegalArgumentException("Failed to parse identifier: " + identifier); - } - } - - public GlobalId(String protocol, String authority, String identifier) { + public GlobalId(String protocol, String authority, String identifier, String separator, String urlPrefix, String providerName) { this.protocol = protocol; this.authority = authority; this.identifier = identifier; + if(separator!=null) { + this.separator = separator; + } + this.urlPrefix = urlPrefix; + this.managingProviderName = providerName; } - public GlobalId(DvObject dvObject) { - this.authority = dvObject.getAuthority(); - this.protocol = dvObject.getProtocol(); - this.identifier = dvObject.getIdentifier(); - } - + // protocol the identifier system, e.g. "doi" + // authority the namespace that the authority manages in the identifier system + // identifier the local identifier part private String protocol; private String authority; private String identifier; + private String managingProviderName; + private String separator = "/"; + private String urlPrefix; /** * Tests whether {@code this} instance has all the data required for a @@ -87,161 +59,50 @@ public String getProtocol() { return protocol; } - public void setProtocol(String protocol) { - this.protocol = protocol; - } - public String getAuthority() { return authority; } - public void setAuthority(String authority) { - this.authority = authority; - } - public String getIdentifier() { return identifier; } - - public void setIdentifier(String identifier) { - this.identifier = identifier; - } + public String getProvider() { + return managingProviderName; + } + public String toString() { return asString(); } /** - * Returns {@code this}' string representation. Differs from {@link #toString} - * which can also contain debug data, if needed. + * Concatenate the parts that make up a Global Identifier. * - * @return The string representation of this global id. + * @return the Global Identifier, e.g. "doi:10.12345/67890" */ public String asString() { if (protocol == null || authority == null || identifier == null) { return ""; } - return protocol + ":" + authority + "/" + identifier; + return protocol + ":" + authority + separator + identifier; } - public URL toURL() { + public String asURL() { URL url = null; if (identifier == null){ return null; } try { - if (protocol.equals(DOI_PROTOCOL)){ - url = new URL(DOI_RESOLVER_URL + authority + "/" + identifier); - } else if (protocol.equals(HDL_PROTOCOL)){ - url = new URL(HDL_RESOLVER_URL + authority + "/" + identifier); - } + url = new URL(urlPrefix + authority + separator + identifier); + return url.toExternalForm(); } catch (MalformedURLException ex) { logger.log(Level.SEVERE, null, ex); - } - return url; - } - - - /** - * Parse a Persistent Id and set the protocol, authority, and identifier - * - * Example 1: doi:10.5072/FK2/BYM3IW - * protocol: doi - * authority: 10.5072 - * identifier: FK2/BYM3IW - * - * Example 2: hdl:1902.1/111012 - * protocol: hdl - * authority: 1902.1 - * identifier: 111012 - * - * @param identifierString - * @param separator the string that separates the authority from the identifier. - * @param destination the global id that will contain the parsed data. - * @return {@code destination}, after its fields have been updated, or - * {@code null} if parsing failed. - */ - private boolean parsePersistentId(String identifierString) { - - if (identifierString == null) { - return false; - } - int index1 = identifierString.indexOf(':'); - if (index1 > 0) { // ':' found with one or more characters before it - int index2 = identifierString.indexOf('/', index1 + 1); - if (index2 > 0 && (index2 + 1) < identifierString.length()) { // '/' found with one or more characters - // between ':' - protocol = identifierString.substring(0, index1); // and '/' and there are characters after '/' - if (!"doi".equals(protocol) && !"hdl".equals(protocol)) { - return false; - } - //Strip any whitespace, ; and ' from authority (should finding them cause a failure instead?) - authority = formatIdentifierString(identifierString.substring(index1 + 1, index2)); - if(testforNullTerminator(authority)) return false; - if (protocol.equals(DOI_PROTOCOL)) { - if (!this.checkDOIAuthority(authority)) { - return false; - } - } - // Passed all checks - //Strip any whitespace, ; and ' from identifier (should finding them cause a failure instead?) - identifier = formatIdentifierString(identifierString.substring(index2 + 1)); - if(testforNullTerminator(identifier)) return false; - } else { - logger.log(Level.INFO, "Error parsing identifier: {0}: '':/'' not found in string", identifierString); - return false; - } - } else { - logger.log(Level.INFO, "Error parsing identifier: {0}: '':'' not found in string", identifierString); - return false; - } - return true; - } - - private static String formatIdentifierString(String str){ - - if (str == null){ - return null; - } - // remove whitespace, single quotes, and semicolons - return str.replaceAll("\\s+|'|;",""); - - /* - < (%3C) -> (%3E) -{ (%7B) -} (%7D) -^ (%5E) -[ (%5B) -] (%5D) -` (%60) -| (%7C) -\ (%5C) -+ - */ - // http://www.doi.org/doi_handbook/2_Numbering.html - } - - private static boolean testforNullTerminator(String str){ - if(str == null) { - return false; } - return str.indexOf('\u0000') > 0; - } - - private boolean checkDOIAuthority(String doiAuthority){ - - if (doiAuthority==null){ - return false; - } - - if (!(doiAuthority.startsWith("10."))){ - return false; - } - - return true; + return null; } + + /** * Verifies that the pid only contains allowed characters. * @@ -257,26 +118,5 @@ public static boolean verifyImportCharacters(String pidParam) { return m.matches(); } - /** - * Convenience method to get the internal form of a PID string when it may be in - * the https:// or http:// form ToDo -refactor class to allow creating a - * GlobalID from any form (which assures it has valid syntax) and then have methods to get - * the form you want. - * - * @param pidUrlString - a string assumed to be a valid PID in some form - * @return the internal form as a String - */ - public static String getInternalFormOfPID(String pidUrlString) { - String pidString = pidUrlString; - if(pidUrlString.startsWith(GlobalId.DOI_RESOLVER_URL)) { - pidString = pidUrlString.replace(GlobalId.DOI_RESOLVER_URL, (GlobalId.DOI_PROTOCOL + ":")); - } else if(pidUrlString.startsWith(GlobalId.HDL_RESOLVER_URL)) { - pidString = pidUrlString.replace(GlobalId.HDL_RESOLVER_URL, (GlobalId.HDL_PROTOCOL + ":")); - } else if(pidUrlString.startsWith(GlobalId.HTTP_DOI_RESOLVER_URL)) { - pidString = pidUrlString.replace(GlobalId.HTTP_DOI_RESOLVER_URL, (GlobalId.DOI_PROTOCOL + ":")); - } else if(pidUrlString.startsWith(GlobalId.HTTP_HDL_RESOLVER_URL)) { - pidString = pidUrlString.replace(GlobalId.HTTP_HDL_RESOLVER_URL, (GlobalId.HDL_PROTOCOL + ":")); - } - return pidString; - } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/GlobalIdServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/GlobalIdServiceBean.java index 0d64c1050b8..aebf13778c3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/GlobalIdServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/GlobalIdServiceBean.java @@ -2,6 +2,8 @@ import static edu.harvard.iq.dataverse.GlobalIdServiceBean.logger; import edu.harvard.iq.dataverse.engine.command.CommandContext; +import edu.harvard.iq.dataverse.pidproviders.PermaLinkPidProviderServiceBean; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; import edu.harvard.iq.dataverse.settings.SettingsServiceBean.Key; import java.util.*; @@ -13,11 +15,28 @@ public interface GlobalIdServiceBean { static final Logger logger = Logger.getLogger(GlobalIdServiceBean.class.getCanonicalName()); - boolean alreadyExists(DvObject dvo) throws Exception; + boolean alreadyRegistered(DvObject dvo) throws Exception; + + /** + * This call reports whether a PID is registered with the external Provider + * service. For providers like DOIs/Handles with an external service, this call + * should accurately report whether the PID has been registered in the service. + * For providers with no external service, the call should return true if the + * PID is defined locally. If it isn't, these no-service providers need to know + * whether use case of the caller requires that the returned value should + * default to true or false - via the noProviderDefault parameter. + * + * @param globalId + * @param noProviderDefault - when there is no external service, and no local + * use of the PID, this should be returned + * @return whether the PID should be considered registered or not. + * @throws Exception + */ + boolean alreadyRegistered(GlobalId globalId, boolean noProviderDefault) throws Exception; - boolean alreadyExists(GlobalId globalId) throws Exception; - boolean registerWhenPublished(); + boolean canManagePID(); + boolean isConfigured(); List getProviderInformation(); @@ -25,15 +44,6 @@ public interface GlobalIdServiceBean { Map getIdentifierMetadata(DvObject dvo); - /** - * Concatenate the parts that make up a Global Identifier. - * @param protocol the identifier system, e.g. "doi" - * @param authority the namespace that the authority manages in the identifier system - * @param identifier the local identifier part - * @return the Global Identifier, e.g. "doi:10.12345/67890" - */ - String getIdentifierForLookup(String protocol, String authority, String identifier); - String modifyIdentifierTargetURL(DvObject dvo) throws Exception; void deleteIdentifier(DvObject dvo) throws Exception; @@ -42,18 +52,27 @@ public interface GlobalIdServiceBean { Map getMetadataForTargetURL(DvObject dvObject); - Map lookupMetadataFromIdentifier(String protocol, String authority, String identifier); - DvObject generateIdentifier(DvObject dvObject); String getIdentifier(DvObject dvObject); boolean publicizeIdentifier(DvObject studyIn); + String generateDatasetIdentifier(Dataset dataset); + String generateDataFileIdentifier(DataFile datafile); + boolean isGlobalIdUnique(GlobalId globalId); + + String getUrlPrefix(); + String getSeparator(); + static GlobalIdServiceBean getBean(String protocol, CommandContext ctxt) { final Function protocolHandler = BeanDispatcher.DISPATCHER.get(protocol); if ( protocolHandler != null ) { - return protocolHandler.apply(ctxt); + GlobalIdServiceBean theBean = protocolHandler.apply(ctxt); + if(theBean != null && theBean.isConfigured()) { + logger.fine("getBean returns " + theBean.getProviderInformation().get(0) + " for protocol " + protocol); + } + return theBean; } else { logger.log(Level.SEVERE, "Unknown protocol: {0}", protocol); return null; @@ -64,8 +83,113 @@ static GlobalIdServiceBean getBean(CommandContext ctxt) { return getBean(ctxt.settings().getValueForKey(Key.Protocol, ""), ctxt); } + public static Optional parse(String identifierString) { + try { + return Optional.of(PidUtil.parseAsGlobalID(identifierString)); + } catch ( IllegalArgumentException _iae) { + return Optional.empty(); + } + } + + /** + * Parse a Persistent Id and set the protocol, authority, and identifier + * + * Example 1: doi:10.5072/FK2/BYM3IW + * protocol: doi + * authority: 10.5072 + * identifier: FK2/BYM3IW + * + * Example 2: hdl:1902.1/111012 + * protocol: hdl + * authority: 1902.1 + * identifier: 111012 + * + * @param identifierString + * @param separator the string that separates the authority from the identifier. + * @param destination the global id that will contain the parsed data. + * @return {@code destination}, after its fields have been updated, or + * {@code null} if parsing failed. + */ + public GlobalId parsePersistentId(String identifierString); + public GlobalId parsePersistentId(String protocol, String authority, String identifier); + + + + public static boolean isValidGlobalId(String protocol, String authority, String identifier) { + if (protocol == null || authority == null || identifier == null) { + return false; + } + if(!authority.equals(GlobalIdServiceBean.formatIdentifierString(authority))) { + return false; + } + if (GlobalIdServiceBean.testforNullTerminator(authority)) { + return false; + } + if(!identifier.equals(GlobalIdServiceBean.formatIdentifierString(identifier))) { + return false; + } + if (GlobalIdServiceBean.testforNullTerminator(identifier)) { + return false; + } + return true; + } + + static String formatIdentifierString(String str){ + + if (str == null){ + return null; + } + // remove whitespace, single quotes, and semicolons + return str.replaceAll("\\s+|'|;",""); + + /* + < (%3C) +> (%3E) +{ (%7B) +} (%7D) +^ (%5E) +[ (%5B) +] (%5D) +` (%60) +| (%7C) +\ (%5C) ++ + */ + // http://www.doi.org/doi_handbook/2_Numbering.html + } + + static boolean testforNullTerminator(String str){ + if(str == null) { + return false; + } + return str.indexOf('\u0000') > 0; + } + + static boolean checkDOIAuthority(String doiAuthority){ + + if (doiAuthority==null){ + return false; + } + + if (!(doiAuthority.startsWith("10."))){ + return false; + } + + return true; + } } + +/* + * ToDo - replace this with a mechanism like BrandingUtilHelper that would read + * the config and create PidProviders, one per set of config values and serve + * those as needed. The help has to be a bean to autostart and to hand the + * required service beans to the PidProviders. That may boil down to just the + * dvObjectService (to check for local identifier conflicts) since it will be + * the helper that has to read settings/get systewmConfig values. + * + */ + /** * Static utility class for dispatching implementing beans, based on protocol and providers. * @author michael @@ -86,5 +210,7 @@ class BeanDispatcher { return null; } }); + + DISPATCHER.put(PermaLinkPidProviderServiceBean.PERMA_PROTOCOL, ctxt->ctxt.permaLinkProvider() ); } } \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java index f4cf38979c5..2f795a4da74 100644 --- a/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.io.OutputStream; import java.text.SimpleDateFormat; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; @@ -910,8 +911,17 @@ public Long getCountGuestbookResponsesByDataFileId(Long dataFileId) { } public Long getCountGuestbookResponsesByDatasetId(Long datasetId) { + return getCountGuestbookResponsesByDatasetId(datasetId, null); + } + + public Long getCountGuestbookResponsesByDatasetId(Long datasetId, LocalDate date) { // dataset id is null, will return 0 - Query query = em.createNativeQuery("select count(o.id) from GuestbookResponse o where o.dataset_id = " + datasetId); + Query query; + if(date != null) { + query = em.createNativeQuery("select count(o.id) from GuestbookResponse o where o.dataset_id = " + datasetId + " and responsetime < '" + date.toString() + "'"); + }else { + query = em.createNativeQuery("select count(o.id) from GuestbookResponse o where o.dataset_id = " + datasetId); + } return (Long) query.getSingleResult(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/HandlenetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/HandlenetServiceBean.java index df16991b51e..d2149a3072a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/HandlenetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/HandlenetServiceBean.java @@ -20,10 +20,12 @@ package edu.harvard.iq.dataverse; +import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import java.io.File; import java.io.FileInputStream; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; @@ -64,14 +66,17 @@ public class HandlenetServiceBean extends AbstractGlobalIdServiceBean { @EJB DataverseServiceBean dataverseService; @EJB - SettingsServiceBean settingsService; + SettingsServiceBean settingsService; private static final Logger logger = Logger.getLogger(HandlenetServiceBean.class.getCanonicalName()); - private static final String HANDLE_PROTOCOL_TAG = "hdl"; - int handlenetIndex = System.getProperty("dataverse.handlenet.index")!=null? Integer.parseInt(System.getProperty("dataverse.handlenet.index")) : 300; + public static final String HDL_PROTOCOL = "hdl"; + int handlenetIndex = JvmSettings.HANDLENET_INDEX.lookup(Integer.class); + public static final String HTTP_HDL_RESOLVER_URL = "http://hdl.handle.net/"; + public static final String HDL_RESOLVER_URL = "https://hdl.handle.net/"; public HandlenetServiceBean() { logger.log(Level.FINE,"Constructor"); + configured = true; } @Override @@ -81,7 +86,7 @@ public boolean registerWhenPublished() { public void reRegisterHandle(DvObject dvObject) { logger.log(Level.FINE,"reRegisterHandle"); - if (!HANDLE_PROTOCOL_TAG.equals(dvObject.getProtocol())) { + if (!HDL_PROTOCOL.equals(dvObject.getProtocol())) { logger.log(Level.WARNING, "reRegisterHandle called on a dvObject with the non-handle global id: {0}", dvObject.getId()); } @@ -226,8 +231,8 @@ private ResolutionRequest buildResolutionRequest(final String handle) { private PublicKeyAuthenticationInfo getAuthInfo(String handlePrefix) { logger.log(Level.FINE,"getAuthInfo"); byte[] key = null; - String adminCredFile = System.getProperty("dataverse.handlenet.admcredfile"); - int handlenetIndex = System.getProperty("dataverse.handlenet.index")!=null? Integer.parseInt(System.getProperty("dataverse.handlenet.index")) : 300; + String adminCredFile = JvmSettings.HANDLENET_KEY_PATH.lookup(); + int handlenetIndex = JvmSettings.HANDLENET_INDEX.lookup(Integer.class); key = readKey(adminCredFile); PrivateKey privkey = null; @@ -268,13 +273,13 @@ private byte[] readKey(final String file) { private PrivateKey readPrivKey(byte[] key, final String file) { logger.log(Level.FINE,"readPrivKey"); - PrivateKey privkey=null; + PrivateKey privkey = null; - String secret = System.getProperty("dataverse.handlenet.admprivphrase"); - byte secKey[] = null; try { + byte[] secKey = null; if ( Util.requiresSecretKey(key) ) { - secKey = secret.getBytes(); + String secret = JvmSettings.HANDLENET_KEY_PASSPHRASE.lookup(); + secKey = secret.getBytes(StandardCharsets.UTF_8); } key = Util.decrypt(key, secKey); privkey = Util.getPrivateKeyFromBytes(key, 0); @@ -309,13 +314,13 @@ private String getAuthenticationHandle(String handlePrefix) { } @Override - public boolean alreadyExists(DvObject dvObject) throws Exception { + public boolean alreadyRegistered(DvObject dvObject) throws Exception { String handle = getDvObjectHandle(dvObject); return isHandleRegistered(handle); } @Override - public boolean alreadyExists(GlobalId pid) throws Exception { + public boolean alreadyRegistered(GlobalId pid, boolean noProviderDefault) throws Exception { String handle = pid.getAuthority() + "/" + pid.getIdentifier(); return isHandleRegistered(handle); } @@ -325,11 +330,6 @@ public Map getIdentifierMetadata(DvObject dvObject) { throw new NotImplementedException(); } - @Override - public HashMap lookupMetadataFromIdentifier(String protocol, String authority, String identifier) { - throw new NotImplementedException(); - } - @Override public String modifyIdentifierTargetURL(DvObject dvObject) throws Exception { logger.log(Level.FINE,"modifyIdentifier"); @@ -347,9 +347,9 @@ public String modifyIdentifierTargetURL(DvObject dvObject) throws Exception { public void deleteIdentifier(DvObject dvObject) throws Exception { String handle = getDvObjectHandle(dvObject); String authHandle = getAuthenticationHandle(dvObject); - - String adminCredFile = System.getProperty("dataverse.handlenet.admcredfile"); - int handlenetIndex = System.getProperty("dataverse.handlenet.index")!=null? Integer.parseInt(System.getProperty("dataverse.handlenet.index")) : 300; + + String adminCredFile = JvmSettings.HANDLENET_KEY_PATH.lookup(); + int handlenetIndex = JvmSettings.HANDLENET_INDEX.lookup(Integer.class); byte[] key = readKey(adminCredFile); PrivateKey privkey = readPrivKey(key, adminCredFile); @@ -383,12 +383,7 @@ private boolean updateIdentifierStatus(DvObject dvObject, String statusIn) { @Override public List getProviderInformation(){ - ArrayList providerInfo = new ArrayList<>(); - String providerName = "Handle"; - String providerLink = "https://hdl.handle.net"; - providerInfo.add(providerName); - providerInfo.add(providerLink); - return providerInfo; + return List.of("Handle", "https://hdl.handle.net"); } @@ -412,7 +407,37 @@ public boolean publicizeIdentifier(DvObject dvObject) { } -} + @Override + public GlobalId parsePersistentId(String pidString) { + if (pidString.startsWith(HDL_RESOLVER_URL)) { + pidString = pidString.replace(HDL_RESOLVER_URL, (HDL_PROTOCOL + ":")); + } else if (pidString.startsWith(HTTP_HDL_RESOLVER_URL)) { + pidString = pidString.replace(HTTP_HDL_RESOLVER_URL, (HDL_PROTOCOL + ":")); + } + return super.parsePersistentId(pidString); + } + @Override + public GlobalId parsePersistentId(String protocol, String identifierString) { + if (!HDL_PROTOCOL.equals(protocol)) { + return null; + } + GlobalId globalId = super.parsePersistentId(protocol, identifierString); + return globalId; + } + + @Override + public GlobalId parsePersistentId(String protocol, String authority, String identifier) { + if (!HDL_PROTOCOL.equals(protocol)) { + return null; + } + return super.parsePersistentId(protocol, authority, identifier); + } + + @Override + public String getUrlPrefix() { + return HDL_RESOLVER_URL; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java index 2bfd342d899..bc7b34ee8b7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java @@ -11,6 +11,7 @@ import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.branding.BrandingUtil; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailServiceBean; +import edu.harvard.iq.dataverse.dataset.DatasetUtil; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.settings.SettingsServiceBean.Key; import edu.harvard.iq.dataverse.util.BundleUtil; @@ -81,37 +82,6 @@ public class MailServiceBean implements java.io.Serializable { public MailServiceBean() { } - public void sendMail(String host, String reply, String to, String subject, String messageText) { - Properties props = System.getProperties(); - props.put("mail.smtp.host", host); - Session session = Session.getDefaultInstance(props, null); - - try { - MimeMessage msg = new MimeMessage(session); - String[] recipientStrings = to.split(","); - InternetAddress[] recipients = new InternetAddress[recipientStrings.length]; - try { - InternetAddress fromAddress = getSystemAddress(); - setContactDelegation(reply, fromAddress); - msg.setFrom(fromAddress); - msg.setReplyTo(new Address[] {new InternetAddress(reply, charset)}); - for (int i = 0; i < recipients.length; i++) { - recipients[i] = new InternetAddress(recipientStrings[i], "", charset); - } - } catch (UnsupportedEncodingException ex) { - logger.severe(ex.getMessage()); - } - msg.setRecipients(Message.RecipientType.TO, recipients); - msg.setSubject(subject, charset); - msg.setText(messageText, charset); - Transport.send(msg, recipients); - } catch (AddressException ae) { - ae.printStackTrace(System.out); - } catch (MessagingException me) { - me.printStackTrace(System.out); - } - } - @Resource(name = "mail/notifyMailSession") private Session session; @@ -177,11 +147,7 @@ public InternetAddress getSystemAddress() { } //@Resource(name="mail/notifyMailSession") - public void sendMail(String from, String to, String subject, String messageText) { - sendMail(from, to, subject, messageText, new HashMap<>()); - } - - public void sendMail(String reply, String to, String subject, String messageText, Map extraHeaders) { + public void sendMail(String reply, String to, String cc, String subject, String messageText) { try { MimeMessage msg = new MimeMessage(session); // Always send from system address to avoid email being blocked @@ -202,18 +168,12 @@ public void sendMail(String reply, String to, String subject, String messageText msg.setSentDate(new Date()); msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to, false)); + if (cc != null) { + msg.setRecipients(Message.RecipientType.CC, InternetAddress.parse(cc, false)); + } msg.setSubject(subject, charset); msg.setText(messageText, charset); - if (extraHeaders != null) { - for (Object key : extraHeaders.keySet()) { - String headerName = key.toString(); - String headerValue = extraHeaders.get(key).toString(); - - msg.addHeader(headerName, headerValue); - } - } - Transport.send(msg); } catch (AddressException ae) { ae.printStackTrace(System.out); @@ -283,11 +243,11 @@ private String getDatasetManageFileAccessLink(DataFile datafile){ } private String getDatasetLink(Dataset dataset){ - return systemConfig.getDataverseSiteUrl() + "/dataset.xhtml?persistentId=" + dataset.getGlobalIdString(); + return systemConfig.getDataverseSiteUrl() + "/dataset.xhtml?persistentId=" + dataset.getGlobalId().asString(); } private String getDatasetDraftLink(Dataset dataset){ - return systemConfig.getDataverseSiteUrl() + "/dataset.xhtml?persistentId=" + dataset.getGlobalIdString() + "&version=DRAFT" + "&faces-redirect=true"; + return systemConfig.getDataverseSiteUrl() + "/dataset.xhtml?persistentId=" + dataset.getGlobalId().asString() + "&version=DRAFT" + "&faces-redirect=true"; } private String getDataverseLink(Dataverse dataverse){ @@ -535,7 +495,7 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio case STATUSUPDATED: version = (DatasetVersion) targetObject; pattern = BundleUtil.getStringFromBundle("notification.email.status.change"); - String[] paramArrayStatus = {version.getDataset().getDisplayName(), (version.getExternalStatusLabel()==null) ? "" : version.getExternalStatusLabel()}; + String[] paramArrayStatus = {version.getDataset().getDisplayName(), (version.getExternalStatusLabel()==null) ? "" : DatasetUtil.getLocaleExternalStatus(version.getExternalStatusLabel())}; messageText += MessageFormat.format(pattern, paramArrayStatus); return messageText; case CREATEACC: @@ -555,7 +515,7 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio case CHECKSUMFAIL: dataset = (Dataset) targetObject; String checksumFailMsg = BundleUtil.getStringFromBundle("notification.checksumfail", Arrays.asList( - dataset.getGlobalIdString() + dataset.getGlobalId().asString() )); logger.fine("checksumFailMsg: " + checksumFailMsg); return messageText += checksumFailMsg; @@ -564,7 +524,7 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio version = (DatasetVersion) targetObject; String fileImportMsg = BundleUtil.getStringFromBundle("notification.mail.import.filesystem", Arrays.asList( systemConfig.getDataverseSiteUrl(), - version.getDataset().getGlobalIdString(), + version.getDataset().getGlobalId().asString(), version.getDataset().getDisplayName() )); logger.fine("fileImportMsg: " + fileImportMsg); @@ -575,7 +535,7 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio messageText = BundleUtil.getStringFromBundle("notification.email.greeting.html"); String uploadCompletedMessage = messageText + BundleUtil.getStringFromBundle("notification.mail.globus.upload.completed", Arrays.asList( systemConfig.getDataverseSiteUrl(), - dataset.getGlobalIdString(), + dataset.getGlobalId().asString(), dataset.getDisplayName(), comment )) ; @@ -586,7 +546,7 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio messageText = BundleUtil.getStringFromBundle("notification.email.greeting.html"); String downloadCompletedMessage = messageText + BundleUtil.getStringFromBundle("notification.mail.globus.download.completed", Arrays.asList( systemConfig.getDataverseSiteUrl(), - dataset.getGlobalIdString(), + dataset.getGlobalId().asString(), dataset.getDisplayName(), comment )) ; @@ -596,7 +556,7 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio messageText = BundleUtil.getStringFromBundle("notification.email.greeting.html"); String uploadCompletedWithErrorsMessage = messageText + BundleUtil.getStringFromBundle("notification.mail.globus.upload.completedWithErrors", Arrays.asList( systemConfig.getDataverseSiteUrl(), - dataset.getGlobalIdString(), + dataset.getGlobalId().asString(), dataset.getDisplayName(), comment )) ; @@ -607,7 +567,7 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio messageText = BundleUtil.getStringFromBundle("notification.email.greeting.html"); String downloadCompletedWithErrorsMessage = messageText + BundleUtil.getStringFromBundle("notification.mail.globus.download.completedWithErrors", Arrays.asList( systemConfig.getDataverseSiteUrl(), - dataset.getGlobalIdString(), + dataset.getGlobalId().asString(), dataset.getDisplayName(), comment )) ; @@ -616,7 +576,7 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio case CHECKSUMIMPORT: version = (DatasetVersion) targetObject; String checksumImportMsg = BundleUtil.getStringFromBundle("notification.import.checksum", Arrays.asList( - version.getDataset().getGlobalIdString(), + version.getDataset().getGlobalId().asString(), version.getDataset().getDisplayName() )); logger.fine("checksumImportMsg: " + checksumImportMsg); @@ -632,7 +592,7 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio messageText = BundleUtil.getStringFromBundle("notification.email.greeting.html"); String ingestedCompletedMessage = messageText + BundleUtil.getStringFromBundle("notification.ingest.completed", Arrays.asList( systemConfig.getDataverseSiteUrl(), - dataset.getGlobalIdString(), + dataset.getGlobalId().asString(), dataset.getDisplayName(), systemConfig.getGuidesBaseUrl(), systemConfig.getGuidesVersion(), @@ -645,7 +605,7 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio messageText = BundleUtil.getStringFromBundle("notification.email.greeting.html"); String ingestedCompletedWithErrorsMessage = messageText + BundleUtil.getStringFromBundle("notification.ingest.completedwitherrors", Arrays.asList( systemConfig.getDataverseSiteUrl(), - dataset.getGlobalIdString(), + dataset.getGlobalId().asString(), dataset.getDisplayName(), systemConfig.getGuidesBaseUrl(), systemConfig.getGuidesVersion(), diff --git a/src/main/java/edu/harvard/iq/dataverse/ManageFilePermissionsPage.java b/src/main/java/edu/harvard/iq/dataverse/ManageFilePermissionsPage.java index 09f067f772c..fd309790026 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ManageFilePermissionsPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/ManageFilePermissionsPage.java @@ -5,6 +5,7 @@ */ package edu.harvard.iq.dataverse; +import edu.harvard.iq.dataverse.api.Util; import edu.harvard.iq.dataverse.authorization.AuthenticationProvider; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.DataverseRole; @@ -20,6 +21,7 @@ import edu.harvard.iq.dataverse.engine.command.impl.AssignRoleCommand; import edu.harvard.iq.dataverse.engine.command.impl.RevokeRoleCommand; import edu.harvard.iq.dataverse.util.BundleUtil; +import edu.harvard.iq.dataverse.util.DateUtil; import edu.harvard.iq.dataverse.util.JsfHelper; import static edu.harvard.iq.dataverse.util.JsfHelper.JH; import java.sql.Timestamp; @@ -34,10 +36,7 @@ import javax.inject.Named; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; -import org.apache.commons.lang3.StringUtils; -import org.primefaces.event.SelectEvent; -import org.primefaces.event.ToggleSelectEvent; -import org.primefaces.event.UnselectEvent; +import org.apache.commons.lang3.ObjectUtils; /** * @@ -83,7 +82,12 @@ public class ManageFilePermissionsPage implements java.io.Serializable { Dataset dataset = new Dataset(); private final TreeMap> roleAssigneeMap = new TreeMap<>(); private final TreeMap> fileMap = new TreeMap<>(); - private final TreeMap> fileAccessRequestMap = new TreeMap<>(); + + public TreeMap> getFileAccessRequestMap() { + return fileAccessRequestMap; + } + + private final TreeMap> fileAccessRequestMap = new TreeMap<>(); private boolean showDeleted = true; public boolean isShowDeleted() { @@ -110,11 +114,6 @@ public TreeMap> getFileMap() { return fileMap; } - public TreeMap> getFileAccessRequestMap() { - return fileAccessRequestMap; - } - - private boolean backingShowDeleted = true; public void showDeletedCheckboxChange() { @@ -125,7 +124,7 @@ public void showDeletedCheckboxChange() { } } - + public String init() { if (dataset.getId() != null) { dataset = datasetService.find(dataset.getId()); @@ -142,17 +141,17 @@ public String init() { initMaps(); return ""; } - + private void initMaps() { // initialize files and usergroup list roleAssigneeMap.clear(); fileMap.clear(); - fileAccessRequestMap.clear(); - + fileAccessRequestMap.clear(); + for (DataFile file : dataset.getFiles()) { - + // only include if the file is restricted (or its draft version is restricted) - //Added a null check in case there are files that have no metadata records SEK + //Added a null check in case there are files that have no metadata records SEK //for 6587 make sure that a file is in the current version befor adding to the fileMap SEK 2/11/2020 if (file.getFileMetadata() != null && (file.isRestricted() || file.getFileMetadata().isRestricted())) { //only test if file is deleted if it's restricted @@ -169,35 +168,67 @@ private void initMaps() { for (RoleAssignment ra : ras) { // for files, only show role assignments which can download if (ra.getRole().permissions().contains(Permission.DownloadFile)) { - raList.add(new RoleAssignmentRow(ra, roleAssigneeService.getRoleAssignee(ra.getAssigneeIdentifier(), true).getDisplayInfo(), fileIsDeleted)); - addFileToRoleAssignee(ra, fileIsDeleted); + raList.add(new RoleAssignmentRow(ra, roleAssigneeService.getRoleAssignee(ra.getAssigneeIdentifier(), true).getDisplayInfo(), fileIsDeleted)); + addFileToRoleAssignee(ra, fileIsDeleted); } } - + file.setDeleted(fileIsDeleted); - + fileMap.put(file, raList); - + // populate the file access requests map - for (AuthenticatedUser au : file.getFileAccessRequesters()) { - List requestedFiles = fileAccessRequestMap.get(au); - if (requestedFiles == null) { - requestedFiles = new ArrayList<>(); - AuthenticatedUser withProvider = authenticationService.getAuthenticatedUserWithProvider(au.getUserIdentifier()); - fileAccessRequestMap.put(withProvider, requestedFiles); - } - requestedFiles.add(file); + for (FileAccessRequest fileAccessRequest : file.getFileAccessRequests()) { + List requestedFiles = fileAccessRequestMap.get(fileAccessRequest.getAuthenticatedUser()); + if (requestedFiles == null) { + requestedFiles = new ArrayList<>(); + AuthenticatedUser withProvider = authenticationService.getAuthenticatedUserWithProvider(fileAccessRequest.getAuthenticatedUser().getUserIdentifier()); + fileAccessRequestMap.put(withProvider, requestedFiles); + } + requestedFiles.add(fileAccessRequest); } - } + } } - } - + public String getAuthProviderFriendlyName(String authProviderId){ - return AuthenticationProvider.getFriendlyName(authProviderId); } - + + Date getAccessRequestDate(List fileAccessRequests){ + if (fileAccessRequests == null) { + return null; + } + + // find the oldest date in the list of available and return a formatted date, or null if no dates were found + return fileAccessRequests.stream() + .filter(fileAccessRequest -> fileAccessRequest.getCreationTime() != null) + .min((a, b) -> ObjectUtils.compare(a.getCreationTime(), b.getCreationTime(), true)) + .map(FileAccessRequest::getCreationTime) + .orElse(null); + } + + public String formatAccessRequestDate(List fileAccessRequests){ + Date date = getAccessRequestDate(fileAccessRequests); + + if (date == null) { + return null; + } + + return DateUtil.formatDate(date); + } + + + public String formatAccessRequestTimestamp(List fileAccessRequests){ + Date date = getAccessRequestDate(fileAccessRequests); + + if (date == null) { + return null; + } + + return Util.getDateTimeFormat().format(date); + } + private void addFileToRoleAssignee(RoleAssignment assignment, boolean fileDeleted) { RoleAssignee ra = roleAssigneeService.getRoleAssignee(assignment.getAssigneeIdentifier()); List assignments = roleAssigneeMap.get(ra); @@ -354,7 +385,10 @@ public void initAssignDialogForFileRequester(AuthenticatedUser au) { fileRequester = au; selectedRoleAssignees = null; selectedFiles.clear(); - selectedFiles.addAll(fileAccessRequestMap.get(au)); + + for (FileAccessRequest fileAccessRequest : fileAccessRequestMap.get(au)) { + selectedFiles.add(fileAccessRequest.getDataFile()); + } showUserGroupMessages(); } @@ -374,20 +408,19 @@ public void grantAccess(ActionEvent evt) { sendNotification = true; } // remove request, if it exist - if (file.getFileAccessRequesters().remove(roleAssignee)) { + if (file.removeFileAccessRequester(roleAssignee)) { datafileService.save(file); - } - } - + } + } } if (sendNotification) { for (AuthenticatedUser au : roleAssigneeService.getExplicitUsers(roleAssignee)) { - userNotificationService.sendNotification(au, new Timestamp(new Date().getTime()), UserNotification.Type.GRANTFILEACCESS, dataset.getId()); + userNotificationService.sendNotification(au, new Timestamp(new Date().getTime()), UserNotification.Type.GRANTFILEACCESS, dataset.getId()); } } } - + initMaps(); } @@ -396,23 +429,31 @@ public void grantAccessToRequests(AuthenticatedUser au) { } public void grantAccessToAllRequests(AuthenticatedUser au) { - grantAccessToRequests(au, fileAccessRequestMap.get(au)); - } + List files = new ArrayList<>(); + + for (FileAccessRequest fileAccessRequest : fileAccessRequestMap.get(au)) { + files.add(fileAccessRequest.getDataFile()); + } + + grantAccessToRequests(au, files); + } private void grantAccessToRequests(AuthenticatedUser au, List files) { boolean actionPerformed = false; // Find the built in file downloader role (currently by alias) DataverseRole fileDownloaderRole = roleService.findBuiltinRoleByAlias(DataverseRole.FILE_DOWNLOADER); for (DataFile file : files) { - if (assignRole(au, file, fileDownloaderRole)) { - file.getFileAccessRequesters().remove(au); - datafileService.save(file); + if (assignRole(au, file, fileDownloaderRole)) { + if (file.removeFileAccessRequester(au)) { + datafileService.save(file); + } actionPerformed = true; } } + if (actionPerformed) { JsfHelper.addSuccessMessage(BundleUtil.getStringFromBundle("permission.fileAccessGranted", Arrays.asList(au.getDisplayInfo().getTitle()))); - userNotificationService.sendNotification(au, new Timestamp(new Date().getTime()), UserNotification.Type.GRANTFILEACCESS, dataset.getId()); + userNotificationService.sendNotification(au, new Timestamp(new Date().getTime()), UserNotification.Type.GRANTFILEACCESS, dataset.getId()); initMaps(); } @@ -423,24 +464,29 @@ public void rejectAccessToRequests(AuthenticatedUser au) { } public void rejectAccessToAllRequests(AuthenticatedUser au) { - rejectAccessToRequests(au, fileAccessRequestMap.get(au)); - } + List files = new ArrayList<>(); + + for (FileAccessRequest fileAccessRequest : fileAccessRequestMap.get(au)) { + files.add(fileAccessRequest.getDataFile()); + } + + rejectAccessToRequests(au, files); + } private void rejectAccessToRequests(AuthenticatedUser au, List files) { - boolean actionPerformed = false; - for (DataFile file : files) { - file.getFileAccessRequesters().remove(au); + boolean actionPerformed = false; + for (DataFile file : files) { + file.removeFileAccessRequester(au); datafileService.save(file); actionPerformed = true; } - if (actionPerformed) { JsfHelper.addSuccessMessage(BundleUtil.getStringFromBundle("permission.fileAccessRejected", Arrays.asList(au.getDisplayInfo().getTitle()))); - userNotificationService.sendNotification(au, new Timestamp(new Date().getTime()), UserNotification.Type.REJECTFILEACCESS, dataset.getId()); + userNotificationService.sendNotification(au, new Timestamp(new Date().getTime()), UserNotification.Type.REJECTFILEACCESS, dataset.getId()); initMaps(); } - } + } private boolean assignRole(RoleAssignee ra, DataFile file, DataverseRole r) { try { diff --git a/src/main/java/edu/harvard/iq/dataverse/ManageTemplatesPage.java b/src/main/java/edu/harvard/iq/dataverse/ManageTemplatesPage.java index 4578a01e693..37ee7948a14 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ManageTemplatesPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/ManageTemplatesPage.java @@ -60,6 +60,9 @@ public class ManageTemplatesPage implements java.io.Serializable { @Inject LicenseServiceBean licenseServiceBean; + + @Inject + SettingsWrapper settingsWrapper; private List